软件设计之美
郑晔
开源项目 Moco 作者
19890 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 42 讲
软件设计之美
15
15
1.0x
00:00/00:00
登录|注册

03 | 可测试性: 一个影响软件设计的重要因素

你好!我是郑晔。
上一讲,我们讲了软件设计的第一步:分离关注点。作为至关重要的第一步,分离关注点常常被人忽略,严重影响了设计的有效性。这一讲,我们再来看另一个经常被很多人忽视的因素:可测试性。
在讨论可测试性之前,我们不妨先来思考一个问题:你觉得软件开发中最浪费时间的环节是什么?答案肯定不是写代码,因为写代码是一个建设的过程,谈不上是在浪费时间。在我接触过的诸多项目里,集成测试可以说是一个浪费时间的大户。
那你的项目是怎么做集成测试的呢?一个常见的测试场景是这样的:你先花了一些时间打包部署一个服务端应用,然后开始测试。测着测着,你发现一个 Bug,然后调查半天,最后发现是一个简单的错误。你就在心里暗恨,为啥写代码的时候没发现呢!
这还只是一个简单的场景,也有稍微复杂一点的。比如,有多个不同项目组的人一起联合测试。当你测出一个 Bug,然后辛辛苦苦调查半天,发现是另外一个模块出了问题,你唯一能做的就是等着那个组的同事把 Bug 改好,测试才能进行下去。更可恨的是,他们查了半天,结果也是一个简单的错误。你会在心里嘀咕,为啥写代码的时候不仔细一点呢?
在实际工作中,我们经常遇到类似的场景。你觉得这种状态正常吗?可能很多人对此习以为常。虽然难受,却不得不忍受。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

软件设计中的可测试性对软件开发过程的效率和质量至关重要。除了功能性需求外,非功能性需求中的可测试性常常被忽视。文章提出了在设计中考虑可测试性的重要性,并指出了一些常见的设计错误,如只有最外层的接口可以测试,整个系统必须集成起来才能测试。为了解决这些问题,文章建议在设计时就要考虑每个模块的测试方式,尽可能给每个模块更多的测试,使构成系统的每个模块尽可能稳定,减少对集成环境的依赖。文章还提到了一些实际案例,如Singleton设计模式和TDD对可测试性的影响,以及Spring框架在简化开发中的作用。总之,可测试性不仅是一个衡量标准,还可以帮助我们理解软件的发展趋势。通过考虑可测试性,可以提前思考软件的质量问题,从而提高软件开发的效率和质量。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《软件设计之美》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(34)

  • 最新
  • 精选
  • Michael
    最近有同事正好在做PDF的生成,也就是把业务数据从各个别的服务拉取回来 然后清洗加工成自己想要的数据 然后传递给模版引擎进行渲染 最终生成pdf文件上传s3 然后通过API把上传的文件地址返回给客户端 想请问老师 这部分逻辑应该怎么测试?因为同事在写完代码之后只做了简单的测试(也就是直接mock其他的service的服务然后mock数据返回 以及mock了s3上传 )最后只是简单看了一下返回值是否为空就完事了。最后到环境上验证才发现好多字段的格式有问题。那像这类场景我们该怎么合理测试才能尽可能保证做到测试完整性呢?

    作者回复: 之所以我们要先讲分离关注点,就是因为很多人会把东西混在一起,测试当然就会很困难了。 以你的场景为例,做 PDF 生成,这里面要拆分开几个不同的环节: * 从别的服务器拉取数据; * 解析外部业务数据; * 业务数据清洗成自己的数据; * 采用模板引擎进行渲染生成 PDF; * 将文件上传到 S3; * 将文件地址返回给客户端。 接下来,就是一个一个分别构建这几个不同的模块,每个模块单独测试。 * 从别的服务器拉取数据:关注数据能否正确获取,获取出错该如何处理,这里需要将拉取协议进行隔离; * 解析外部业务数据:关注数据能否正确解析,无法解析的数据该如何处理; * 业务数据清洗成自己的数据:关注数据能否正确转化,业务含义不正确该如何处理; * 采用模板引擎进行渲染生成PDF:关注渲染过程能否正常进行。需要将PDF作为一个生成目标进行隔离。这一过程需要人工检查生成的PDF格式是否正确。 * 将文件上传到S3:关注文件上传是否正确,需要将文件上传目标进行隔离,S3只是一个目标。 * 将文件地址返回给客户端:关注是否能够获取到文件地址,这里要结合上一项中的上传目标,将S3隔离开来。 各个都测试好之后,再进行集成测试,这里面的关注点就是这些模块联动起来是否能够正常运行,以及检查结果的正确性。

    2020-05-29
    10
    117
  • Moonus
    比如service层有个很长,包含复杂逻辑的private方法,但我又想测试他。只能通过最顶层的public方法作为入口,这导致需要保证大量的前提条件的正确,我们需要mock很多外部依赖。 当private方法复杂并包含逻辑,其正确应当重构代码,而不是在测试做妥协,可以将需要测试的private方法转移到另一个对象中,成为一个public的方法。同时让我体会到测试驱动设计的含义。

    作者回复: private 方法怎么测?其实是一个伪命题。要测 private 方法,更多的是因为这个类承担了过多的职责,才会出现层层嵌套的方法,才会不好测。 你的改进方法非常对,将这个方法移到另外一个类中,它成了 public 的,该怎么测,就怎么测。

    2020-05-29
    5
    31
  • 段启超
    和大家分享一下我最近发现的一个问题:滥用@Autowired 最近在给手上的代码上加测试的时候我进行了如下操作: 1. 我在测试类中写了一个测试,因为类A中要执行的这个逻辑需要依赖另外一个对象B,于是我把这个需要依赖的对象B从全局变量中移动到构造中,作为参数放了进去,然后再构造方法上加了注解@Autowired。(注意: 原来的这个@Autowired 是在这个全局变量上的) 2. 我在测试中mock 了这个依赖的B对象,然后给到了这个Service 的实例中去。 3. 写业务代码实现,让测试通过。 本来以为测试已经通过,没什么问题了,结果在集成测试启动Spring 容器的时候,发现挂掉了,发生了循环依赖,查看代码,果然,在B中也@Autowired 注入了A ,他们两个就此开始循环~~~~ 结果用idea 的分析工具扫了一通,发现循环依赖不止于此,还有很多很多。。。。。 从这件事儿上体现出来的问题: 1: 滥用@Autowired 体现出来的首先就是没有分离关注点,让一个类中做了过多的事情。 现在想过来原来写过的很多代码里头都是一大堆的@Autowired 注入 ,很多XXXService 的代码大概率的会发生这种事情。 2: 其实IDEA 编译器里边早就提醒过你了,当你在字段上使用@Autowired 去注入的时候,会有黄线的提示,提醒你不要这么使用,推荐你使用构造器的方式去注入。而很多人的处理方式就是对这个黄线视而不见。 3:如果从代码的测试性的角度来出发,你一定不会这么做,因为这么做没有办法去mock 你的依赖。 代码会丑到你自己都不想看。 4: 以前没想过循环依赖是怎么发生的,现在明白了:一个罪魁祸首就是滥用@Autowired 。循环依赖本身也是一个设计上的坏味道。如果有人问我说如何解决循环依赖,我会回答他: 首先不要循环依赖。

    作者回复: 多谢分享如此惨痛的教训,相信你已经理解了循环依赖的问题所在。 从设计的角度看,循环依赖之所以产生,一个重要的原因就是没有分清接口和实现。如果都是实现,一不小心就循环依赖了,如果分清楚接口和实现,实现依赖于接口,产生循环依赖的可能性就会大幅度降低。

    2020-05-31
    2
    21
  • 西西弗与卡夫卡
    曾经开发过堆场应用,其中一个步骤是从远端服务器同步到本地服务器,然后再执行本地逻辑。如果每次测试本地逻辑都要从服务端拉取数据的话,就没法自动测了。当时采用的测试方法就是先抓取接口数据生成接口文件,测试就从文件中加载,再运行,最后销毁整个数据库。如果有接口相关的bug,也同样抓取数据保存,构建一个bug号命名的测试方法测试bug。 后来做过系统高可用软件,采用的方法是将代码自动部署到多个Docker里,测试代码里依据场景(为了方便,场景还用DSL写)比如杀某个Docker来测试高可用逻辑是否正常。

    作者回复: 很赞的做法!

    2020-05-29
    18
  • 阳仔
    其实难度是,现在大多开发者接触的一个已有的系统,在这个系统进行维护,修改各种bug,以及添加一些新需求。那么问题来了,如何将一个已有系统改造成粒度小且可测试的程序?我觉得这个应该是大家关心的工程实践

    作者回复: 如何将一个已有系统改造成粒度小且可测试的程序?这到底是不是一个问题,其实要考虑的。很多系统的改造,是切分开来,逐步替换。我们在专栏最后,会讲到如何做一个现有系统的改造,敬请期待。

    2020-05-29
    6
    15
  • Jxin
    1.对于自己重构大半代码的项目,我觉得应该是有偏见的(恶心太多次了)。但我还是认为我现在的项目可测试性很差。 a.项目启动太慢(服务包启动一次7-8分钟,war包启动一次20几分钟)。虽然单测时我们可以控制扫包范围,但总会扫到一些"基础包"莫名其妙的玩意,逼着你只能扩大扫包范围,而扩大扫包范围就会让单元测试的间隔无意义的变长。(项目本身分层没规范。部分基础公共包的分层不规范,且强依赖,无法按需加载) b.工具包大量非纯函数的工具。静态方法不采用mock框架的高级功能是mock不了的,所以往往我们不会去测试工具类的逻辑。但这些工具包往往也存在文档不全或文档和功能脱节的情况。这个时候,一旦你使用不当或者工具类有bug那么就可能出现与预期不符的情况,而这个排查起来是比较难受的。因为这往往是你排查的最后一步,而你可能没有权限,只能看着反编译的源码,找到问题也改不了。只能改动自己的功能不用这个工具。(由此可见DI是多么美好的东西) c.臃肿的大类+强依赖中间数据结构。近百个字段的核心类,让你找个字段都很晕。中间数据(map,键值的list)在功能间传递,缺少封装。每次要获取自己要用的数据结构都要对中间数据做转换。逻辑走完后,还要对中间数据做变动,以保证变更对外部生效。这一切的结果就是逻辑被变得复杂了,代码行数增多了。而代码行数和逻辑变多出问题的可能就变大,测试的工作量就变大。 2.其他的,诸如不要有未决行为,尽量少用全局变量,低耦合等等都是个人编码能力的事,怪不得老项目了。提高可测试性,其实我觉得就是要遵循最小依赖的原则。最好就只有入参和本地逻辑(逻辑中没有外部依赖,没有外部变量,没有外部静态方法),这样可测试性是最好的。而要做到这个也并非不可能,全在你的设计和拆分力度上。

    作者回复: 很好的思考,你发现的问题确实都是很严重的问题,会严重地影响开发的进展。不过,能发现这些问题,就是最好改进的起点。

    2020-05-29
    8
  • 北天魔狼
    测试前置数据,测试API,测试完清理数据。 感觉测试主要是为了防止自己或者别人改了自己的代码

    作者回复: 没有谁是可靠的,包括自己。有几个人能记住自己几个月前写的代码细节呢。

    2020-05-29
    6
  • 业余爱好者
    由于集成测试环境复杂,排错复杂,所以我们要尽量在前期,开发甚至设计阶段就要考虑测试,尽量把bug消灭在集成测试之前。每个模块保证接口功能正常,模块交互又是按照规范的,一般集成都问题不大,当然只是概率非常低。 单元测试还有一个好处,那就是自动化。写一个功能,就运行一下单元测试。单元测试是代码功能的保障。经常对曾经一些低级的bug如手误少些个!之类的错误深恶痛绝,觉得怎么能犯这么低级的错误。我错怪自己了,人就是这样一种粗心的动物,应该让程序来检查代码的正确性,程序检查的又快又不会出错,何乐不为?当然前提是得有单元测试。 Tdd是一种好的实践。在测试上要多下点功夫。

    作者回复: 这个理解是对的。做好 TDD,前提是懂一点设计,否则,自己就把自己绕进去了。

    2020-05-29
    6
  • 阳仔
    前面一节提到软件设计的首先是要分解,而分解的一个目的我觉得就是为了软件的可测试性。 当然这也是建立在分解粒度要小的基础上,

    作者回复: 没错,这是一脉相承的。

    2020-05-29
    5
  • PM2
    有什么好的资源关于单元测试吗?网上内容虽然很多,但是鱼龙混杂

    作者回复: 我的单元测试并不是靠资料学出来的,那就推荐一本很多人推荐的书,《测试驱动的面向对象软件开发》(Growing Object-Oriented Software, Guided by Tests)。

    2020-06-02
    2
    4
收起评论
显示
设置
留言
34
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部