主页

索引

模块索引

搜索页面

程序员的测试课

备注

郑晔:在 “极客时间” 里的专栏,分别是《10x 程序员工作法》、《软件设计之美》、《代码之丑》

  • 开发代码之前要做任务分解,这是《10x 程序员工作法》讲过的工作原则;

  • 代码要可测,这是《软件设计之美》讲过的衡量设计优劣的一个重要标准;

  • 代码要小巧,这是《代码之丑》讲过的代码追求的目标;

开篇词

开篇词 _ 为什么写测试是程序员的本职工作

  • 测试是为了验证是否符合预期

  • 面向设计的接口去测试,不要面向实现去测试:否则,如果测试和实现关联太紧了,项目在不停的重构,然后测试代码也在不停的改,所以写到后面测试代码就不想写了。

基础篇 (11 讲)

01 | 实战: 实现一个 ToDo 的应用

  • 设计先行

  • 任务分解

  • 编写测试

https://img.zhaoweiguo.com/uPic/2022/09/zyrKs7.jpg
  • 如果今天的内容你只能记住一句话,那么请记住: 细化测试场景,编写可测试的代码

留言

  • 这个例子比较简单,挺适合刚开始入门测试的人,但是同时,也可能会让开发觉得,这么简单的逻辑,一看就知道对不对、有没有问题了,何必还要用自动化测试进行覆盖呢

  • 一方面,这确实只是个例子,但是也不能因为简单,就不写用例,因为所有的代码都不可预知的在后面发生改变,那么作为后续的回归用例,也是一样有用的;另一方面,需要关注 mock 的使用,实际业务场景很复杂,函数之间相互调用逻辑更是很常见,所以会经常用到 mock,但是 mock 也不可避免的会让我们规避了集成测试的测试点,所以怎么选取合适的 mock 点,并预知 mock 的风险,也是要了解的。再补充,就算自己写了测试用例,也不要完全的依赖测试来发现所有的问题,代码思维、设计风格、编码习惯,这些预防问题发生的手段,才是最有效的。

  • 作者回复:很多人是简单的事情做不好,复杂的事情搞不定

  • 我在实现例如 TodoItemService 之前,通常都会先测试它的 输入和输出,两个类被测试完成后,我才去开始用测试实现 TodoItemService. 但看了您的实现手法,一个测试集就已经能够覆盖到了输入输出。感觉您这样更简单些。不知道我想的对不对,还是应该一个模块对应一个测试集呢

  • 作者回复:围绕着目标去做测试,我没有特别的想过输入和输出。

02 _ 实战: 实现一个 ToDo 的应用

  • 为了保证测试是可以重复执行的,我们要确保所有的资源在执行之后要恢复原样。

  • 我们测试的目标是我们的代码,而不是这个难以测试的程序库。

  • 由于其它程序库造成难以测试的问题,我们可以做一层层薄薄的封装,然后,在覆盖率检查中忽略它。封装和忽略,缺一不可。

  • 写测试要尽可能减少对于不稳定组件的依赖

https://img.zhaoweiguo.com/uPic/2022/09/Q9hkwH.jpg
  • 如果今天的内容你只能记住一件事,那请记住:隔离变化,逐步编写稳定的代码。

03 | 程序员的测试与测试人员的测试有什么不同

  • 视角不同: 程序员的出发点是实现,而测试人员的出发点是业务。

  • 程序员的关注点是白盒测试,而测试人员则是黑盒测试。

  • 从业务角度思考,确实是我们向测试人员学习的一个重要方向。

  • 测试人员把用例分享给程序员,程序员用代码固化新的测试用例

  • 如果今天的内容你只能记住一件事,那请记住:测试从测试场景入手,多考虑各种情况,尤其是异常情况。

04 | 自动化测试:为什么程序员做测试其实是有优势的

测试框架简介

理解自动化测试框架,主要包含两个部分:

组织测试的结构以及断言

测试结构:

@BeforeEach  setUp
TestXXX
@AfterEach  tearDown

断言:

测试结构保证了测试用例能够按照预期的方式执行,
而断言则保证了我们的测试需要有一个目标,也就是我们到底要测什么。
  • 如果今天的内容你只能记住一件事,那请记住:没有断言的测试不是好测试。

05 | 一个好的自动化测试长什么样

https://img.zhaoweiguo.com/uPic/2022/09/mOAvI3.jpg
  • 给测试写测试不是一个行得通的做法,那唯一可行的方案就是,把测试写简单,简单到一目了然,不需要证明它的正确性。

测试分成了四段,分别是:

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 ());
https://img.zhaoweiguo.com/uPic/2022/09/iLpW32.jpg

Gerard Meszaros 写过一本《xUnit Test Patterns》,他给这些名词起了一个统一的名字,形成了一个新的模式:Test Double(测试替身)。然而,这个名字也没有在业界得到足够广泛的传播,你更熟悉的说法应该是 Mock 对象。因为后来在这个模式广泛流行起来之前,Mock 框架先流行了起来。

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 _ 测试应该怎么配比

https://img.zhaoweiguo.com/uPic/2022/09/iHHg21.jpg

冰淇淋蛋卷模型:很多遗留项目是没有测试的,补测试是希望能够快速地建立起安全网,那必然是从系统测试入手来得快。只要写上一些高层测试,就能够覆盖到系统的大部分功能,属于 “投资少见效快” 的做法。这也是很多人喜欢冰淇淋蛋卷模型的重要原因。

  • 如果今天的内容你只能记住一件事,那请记住:新项目采用测试金字塔,遗留项目从冰淇淋蛋卷出发。

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 就是先写测试后写代码吗

https://img.zhaoweiguo.com/uPic/2022/09/PpOTtH.jpg

TDD 的节奏:红 - 绿 - 重构:测试先行开发和测试驱动开发的差异就在重构上。

在测试驱动开发中,重构与测试是相辅相成的:没有测试,修改代码只能是提心吊胆;没有重构,代码的混乱程度会逐步增加,测试也会变得越来越不好写。

  • 测试驱动开发要从任务分解开始。

  • 很多懂 TDD 的人会把 TDD 解释为测试驱动设计(Test Driven Design)。

为了写测试,首先 “驱动” 着我们把需求分解成一个一个的任务,然后会 “驱动” 着我们给出一个可测试的设计,而在具体的写代码阶段,又会 “驱动” 着我们不断改进写出来的代码。把这些内容结合起来看,我们真的是在用测试 “驱动” 着开发。

  • 如果今天的内容你只能记住一件事,那请记住:从测试的视角出发看待代码。

18 _ BDD 是什么东西

  • BDD 的全称是 Behavior Driven Development,也就是行为驱动开发

  • BDD 的用例更多偏向业务视角

  • 如果今天的内容你只能记住一件事,那请记住:技术团队要更加贴近业务。

结束语

答疑解惑 _ 那些东西怎么测

  • 效用的好坏要依赖于反馈:数据模型的有效性要靠业务来反馈,软件的好用还是好看要靠用户来反馈。

  • 测试固然有用,但它不是万能的。作为程序员,我们只有分辨清楚自己面对的究竟是什么问题,才能使用相应的工具去解决问题。

结束语 _ 对代码的信心要从测试里来

《清单革命》,作者是阿图・葛文德,他是一名医生,曾是白宫最年轻的健康政策顾问:

人类的错误可以分为两大类型。
第一类是 “无知之错”,我们犯错是因为没有掌握相关知识。
第二类是 “无能之错”,我们犯错并非因为没有掌握相关知识,而是因为没有正确使用这些知识。
无知之错,可以原谅,无能之错,不可原谅。
  • 如果整个专栏你只能记住一件事,那请记住:写代码时问问自己,这段代码应该怎么测。

主页

索引

模块索引

搜索页面