程序员的测试课¶
备注
郑晔:在 “极客时间” 里的专栏,分别是《10x 程序员工作法》、《软件设计之美》、《代码之丑》
开发代码之前要做任务分解,这是《10x 程序员工作法》讲过的工作原则;
代码要可测,这是《软件设计之美》讲过的衡量设计优劣的一个重要标准;
代码要小巧,这是《代码之丑》讲过的代码追求的目标;
开篇词¶
开篇词 _ 为什么写测试是程序员的本职工作¶
测试是为了验证是否符合预期
面向设计的接口去测试,不要面向实现去测试:否则,如果测试和实现关联太紧了,项目在不停的重构,然后测试代码也在不停的改,所以写到后面测试代码就不想写了。
基础篇 (11 讲)¶
01 | 实战: 实现一个 ToDo 的应用¶
设计先行
任务分解
编写测试
如果今天的内容你只能记住一句话,那么请记住: 细化测试场景,编写可测试的代码
留言¶
这个例子比较简单,挺适合刚开始入门测试的人,但是同时,也可能会让开发觉得,这么简单的逻辑,一看就知道对不对、有没有问题了,何必还要用自动化测试进行覆盖呢
一方面,这确实只是个例子,但是也不能因为简单,就不写用例,因为所有的代码都不可预知的在后面发生改变,那么作为后续的回归用例,也是一样有用的;另一方面,需要关注 mock 的使用,实际业务场景很复杂,函数之间相互调用逻辑更是很常见,所以会经常用到 mock,但是 mock 也不可避免的会让我们规避了集成测试的测试点,所以怎么选取合适的 mock 点,并预知 mock 的风险,也是要了解的。再补充,就算自己写了测试用例,也不要完全的依赖测试来发现所有的问题,代码思维、设计风格、编码习惯,这些预防问题发生的手段,才是最有效的。
作者回复:很多人是简单的事情做不好,复杂的事情搞不定
我在实现例如 TodoItemService 之前,通常都会先测试它的 输入和输出,两个类被测试完成后,我才去开始用测试实现 TodoItemService. 但看了您的实现手法,一个测试集就已经能够覆盖到了输入输出。感觉您这样更简单些。不知道我想的对不对,还是应该一个模块对应一个测试集呢
作者回复:围绕着目标去做测试,我没有特别的想过输入和输出。
02 _ 实战: 实现一个 ToDo 的应用¶
为了保证测试是可以重复执行的,我们要确保所有的资源在执行之后要恢复原样。
我们测试的目标是我们的代码,而不是这个难以测试的程序库。
由于其它程序库造成难以测试的问题,我们可以做一层层薄薄的封装,然后,在覆盖率检查中忽略它。封装和忽略,缺一不可。
写测试要尽可能减少对于不稳定组件的依赖
如果今天的内容你只能记住一件事,那请记住:隔离变化,逐步编写稳定的代码。
03 | 程序员的测试与测试人员的测试有什么不同¶
视角不同: 程序员的出发点是实现,而测试人员的出发点是业务。
程序员的关注点是白盒测试,而测试人员则是黑盒测试。
从业务角度思考,确实是我们向测试人员学习的一个重要方向。
测试人员把用例分享给程序员,程序员用代码固化新的测试用例
如果今天的内容你只能记住一件事,那请记住:测试从测试场景入手,多考虑各种情况,尤其是异常情况。
04 | 自动化测试:为什么程序员做测试其实是有优势的¶
测试框架简介¶
理解自动化测试框架,主要包含两个部分:
组织测试的结构以及断言
测试结构:
@BeforeEach 和 setUp
TestXXX
@AfterEach 和 tearDown
断言:
测试结构保证了测试用例能够按照预期的方式执行,
而断言则保证了我们的测试需要有一个目标,也就是我们到底要测什么。
如果今天的内容你只能记住一件事,那请记住:没有断言的测试不是好测试。
05 | 一个好的自动化测试长什么样¶
给测试写测试不是一个行得通的做法,那唯一可行的方案就是,把测试写简单,简单到一目了然,不需要证明它的正确性。
测试分成了四段,分别是:
1. 准备
2. 执行
3. 断言
4. 清理
一段旅程(A-TRIP):
1. Automatic,自动化
自动化测试相比传统测试,核心增强就在自动化上。
2. Thorough,全面的
应该尽可能用测试覆盖各种场景
全面还有一个角度,就是测试覆盖率
3. Repeatable,可重复的
每次执行结果相同
影响一个测试可重复性的主要因素是外部资源
常见的外部资源包括文件、数据库、中间件、第三方服务等等
测试关键:
在测试结束后,恢复原来的样子
第三方服务,则可以采用模拟服务Mock
4. Independent,独立的
一个测试不依赖于其他测试先执行
5. Professional,专业的
测试代码也是代码,也要按照代码的标准去维护
如果今天的内容你只能记住一件事,那请记住:编写简单的测试。
06 | 测试不好做,为什么会和设计有关系¶
可测试性¶
软件设计就是在构建模型和规范
对一个可测试性好的系统而言,应该每个模块都可以进行独立的测试
有了相应的测试做保证,才敢于放手去改
编写可测试的代码¶
让自己的代码符合软件设计原则
- 编写可测试的代码,如果只记住一个通用规则,那就是编写可组合的代码
第一个推论是不要在组件内部去创建对象
第二个推论:不要编写 static 方法(还有两样东西你也就可以抛弃了,一个是全局状态,一个是 Singleton 模式)
与第三方代码集成¶
调用程序库:
做代码隔离,需要先定义接口,然后,用第三方代码去做一个相应的实现
=> 依赖倒置原则
由框架回调:
回调代码只做薄薄的一层,负责从框架代码转发到业务代码。
=> 一种常见的模式:防腐层
如果今天的内容你只能记住一件事,那请记住:编写可测试的代码。
07 _ Mock 框架: 怎么让测试变得可控¶
从模式到框架¶
做测试,本质上就是在一个可控的环境下对被测系统 / 组件进行各种试探。
怎么把不可控变成可控:
第一步自然是隔离
第二步就是用一个可控的组件代替不可控的组件。换言之,用一个假的组件代替真的组件。
这种用假组件代替真组件的做法很多不同的名词,比如:
Stub、Dummy、Fake、Spy、Mock
Stub:
有个待测的组件 A,内部依赖组建 B, 会执行 B.callFunc ();
Stub 做的事情是模拟一个 B 对象,设置好模拟 B 对象的 callFunc () 这个方法的输入与输出即可
Mock:
Mock,除了上述的安排好输入与输出之外,还要对 B.callFunc () 这个方法本身的行为做校验
比如 verify (C,atLeast (1)).callMethod (any ());
Mock 框架¶
它最核心的两个点:
1. 设置模拟对象
2. 校验对象行为
设置 Mock 对象:
参数是什么样的
对应的处理是什么样的
校验对象行为:
测试应该测试的是接口行为,而不是内部实现
如果按照测试模式来说,设置 Mock 对象的行为应该算是 Stub,而校验对象行为的做法,才是 Mock
按照模式的说法,我们应该常用 Stub,少用 Mock
Mock 框架的延伸¶
Mock 框架的主要作用是模拟对象的行为,但作为一种软件设计思想,它却有着更大的影响。既然我们可以模拟对象行为,那本质上来说,我们也可以模拟其它东西。
如:开源模拟服务器程序库——Moco
总结¶
如果今天的内容你只能记住一件事,那请记住:使用 Mock 框架,少用 verify。
08 _ 单元测试应该怎么写¶
单元测试什么时候写¶
最好能够将代码和测试一起写。
- 想写好单元测试也要从任务分解开始。
需要把一个要完成的需求拆分成很多颗粒度很小的任务。
粒度要小到可以在很短时间内完成,比如,半个小时就可以写完。
编写单元测试的过程¶
先设计测试用例,后写代码(专注于细节时,有限的注意力就会让你忽略掉很多东西)
只要我们完成一个子任务,我们就可以做一次代码的提交,因为我们这个时候,既有测试代码又有实现代码,而且实现代码是通过了测试的。
编写单元测试的过程,实际上就是一个任务开发的过程。一个任务代码的完成,不仅仅是写了实现代码,还要通过相应的测试。一般而言,任务开发要先设计相应的接口,确定其行为,然后根据这个接口设计相应的测试用例,最后,把这些用例实例化成一个个具体的单元测试。
测接口还是测实现¶
在实际的项目中,我会更倾向于测试接口,尽可能减少对于实现细节的约束。
单元测试常见的一个问题是代码一重构,单元测试就崩溃。这很大程度上是由于测试对实现细节的依赖过于紧密。一般来说,单元测试最好是面向接口行为来设计,因为这是一个更宽泛的要求。其实,在测试中的很多细节也可以考虑设置得宽泛一些,比如模拟对象的设置、模拟服务器的设置等等。
总结¶
如果今天的内容你只能记住一件事,那请记住:做好任务分解,写好单元测试。
09 _ 测试覆盖率: 如何找出没有测试到的代码¶
测试覆盖率¶
测试覆盖率是一种度量指标,指的是在运行一个测试集合时,代码被执行的比例。它的一个主要作用就是告诉我们有多少代码测试到了。
常见的测试覆盖率指标有下面这几种(更详细参见「软件测试52讲」):
1. 函数覆盖率(Function coverage):代码中定义的函数有多少得到了调用;
2. 语句覆盖率(Statement coverage):代码中有多少语句得到了执行;
3. 分支覆盖率(Branches coverage):控制结构中的分支有多少得到了执行(比如 if 语句中的条件);
4. 条件覆盖率(Condition coverage):每个布尔表达式的子表达式是否都检查过 true 和 false 的不同情况;
5. 行覆盖率(Line coverage):代码中有多少行得到了测试。
总结¶
如果今天的内容你只能记住一件事,那请记住:将测试覆盖率的检查加入到自动化过程之中。
10 _ 为什么 100% 的测试覆盖率是可以做到的¶
100% 的测试覆盖率¶
保证自己编写的代码 100% 测试覆盖:
首先,让自己可控的代码有完全的测试保证,
其次,如果有第三方的代码影响到测试覆盖,我们应该把第三方的代码和我们的代码隔离开。
一个新项目想要达到 100% 的测试覆盖,首先,要有可测试的设计,要能够编写整洁的代码;其次,测试和代码同步写。
要想做到 100% 的测试覆盖,技术上说,要有可测试的设计以及编写整洁的代码,实践上看,要测试和代码同步产出。
测不到的代码¶
100% 的测试覆盖并不是说代码没有问题了,而应该是程序员对自己编写代码的一种质量保证,它是一个帮助我们查缺补漏的过程
对于无法测试到第三方代码,要用一个薄薄的隔离层将代码隔离出去,在构建脚本中将隔离层排除在外。
总结时刻¶
如果今天的内容你只能记住一件事,那请记住:100% 的测试覆盖率是程序员编写高质量代码的保证。
11 _ 集成测试: 单元测试可以解决所有问题吗¶
一种是代码之间的集成,一种是代码与外部组件的集成。说白了,集成测试就是把不同的组件组合到一起,看看它们是不是能够很好地配合到一起。
相对于单元测试只关注单元行为,集成测试关注的多个组件协同工作的表现。
代码的集成¶
对代码之间的集成来说,一方面要考虑我们自己编写的各个单元如何协作;另一方面,在使用各种框架的情况下,要考虑与框架的集成。如果我们有了单元测试,这种集成主要是关心链路的通畅,所以一般来说我们只要沿着一条执行路径,把相关的代码组装到一起进行测试就可以了。
使用了框架,最好能把框架集成进去做一个完整的集成测试。
一个框架设计得好坏与否,对测试的支持程度也是一个很重要的衡量标准,这能很好地体现出框架设计者的品味。
嵌入式容器
集成外部组件¶
外部组件集成,难点就在于外部组件的状态如何控制。
引入了第三方服务,可以使用 Mock,也可以使用服务对应的成熟解决方案(比如数据库相关)来满足集成测试的需要。
总结¶
如果今天的内容你只能记住一件事,那请记住:想办法将不同组件集成起来进行测试。
应用篇 (5 讲)¶
12 _ 实战: 将 ToDo 应用扩展为一个 REST 服务¶
如果今天的内容你只能记住一句话,那么请记住,集成测试回滚数据,保证测试的可重复性。
13 _ 在 Spring 项目中如何进行单元测试¶
如果今天的内容你只能记住一件事,那请记住:业务代码不要过度依赖于框架。
14 _ 在 Spring 项目如何进行集成测试¶
做测试的一个关键点就是不能随意修改代码,切记,不能为了测试的需要而修改代码。
要保证数据库的可重复性有两种做法:
1. 嵌入式内存数据库
注意: 不同的数据库上都会有的问题,也就是 SQL 的不一致
2. 事务回滚
如果今天的内容你只能记住一件事,那请记住:采用轻量级的测试手段,保证代码的正确性。
15 _ 测试应该怎么配比¶
如果今天的内容你只能记住一件事,那请记住:新项目采用测试金字塔,遗留项目从冰淇淋蛋卷出发。
16 _ 怎么在遗留系统上写测试¶
备注
在各种遗留系统的定义中,Michael Feathers 在《修改代码的艺术》(Working Effectively with Legacy Code)中给出的定义让我印象最为深刻 —— 遗留系统就是没有测试的系统。
想在遗留系统中写好测试,一个关键点就是解耦。 * 把技术代码和业务代码分离开
最精髓的部分:先隔离,再分离。
重构:
提取方法(Extract Method)
提取委托(Extract Delegate)
生成访问器(Generate Accessors)
搬移实例方法(Move Instance Method)
提取接口(Extract Interface)
《修改代码的艺术》(Working Effectively with Legacy Code)。虽然它是一本介绍处理遗留代码的书,在我看来,它更是一本教人如何写测试的书。
如果今天的内容你只能记住一件事,那请记住:改造遗留系统的关键是解耦。
03扩展篇 (2 讲)¶
17 _ TDD 就是先写测试后写代码吗¶
在测试驱动开发中,重构与测试是相辅相成的:没有测试,修改代码只能是提心吊胆;没有重构,代码的混乱程度会逐步增加,测试也会变得越来越不好写。
测试驱动开发要从任务分解开始。
很多懂 TDD 的人会把 TDD 解释为测试驱动设计(Test Driven Design)。
为了写测试,首先 “驱动” 着我们把需求分解成一个一个的任务,然后会 “驱动” 着我们给出一个可测试的设计,而在具体的写代码阶段,又会 “驱动” 着我们不断改进写出来的代码。把这些内容结合起来看,我们真的是在用测试 “驱动” 着开发。
如果今天的内容你只能记住一件事,那请记住:从测试的视角出发看待代码。
18 _ BDD 是什么东西¶
BDD 的全称是 Behavior Driven Development,也就是行为驱动开发
BDD 的用例更多偏向业务视角
如果今天的内容你只能记住一件事,那请记住:技术团队要更加贴近业务。
结束语¶
答疑解惑 _ 那些东西怎么测¶
效用的好坏要依赖于反馈:数据模型的有效性要靠业务来反馈,软件的好用还是好看要靠用户来反馈。
测试固然有用,但它不是万能的。作为程序员,我们只有分辨清楚自己面对的究竟是什么问题,才能使用相应的工具去解决问题。
结束语 _ 对代码的信心要从测试里来¶
《清单革命》,作者是阿图・葛文德,他是一名医生,曾是白宫最年轻的健康政策顾问:
人类的错误可以分为两大类型。
第一类是 “无知之错”,我们犯错是因为没有掌握相关知识。
第二类是 “无能之错”,我们犯错并非因为没有掌握相关知识,而是因为没有正确使用这些知识。
无知之错,可以原谅,无能之错,不可原谅。
如果整个专栏你只能记住一件事,那请记住:写代码时问问自己,这段代码应该怎么测。