《Java 测试驱动开发》学习笔记

如何写好代码

1 为何要关系测试驱动开发

1.1.2 红灯-绿灯-重构

在重构阶段,我们不修改任何功能,也不映入新的测试,而只改进代码,并不断运行所有测试,确保没有破坏任何功能

1.1.4 TDD 并非测试方法

💡 测试驱动开发的主要目标是提供可测试的代码设计,测试只是一项很有用的副产品

1.5 无需调试

💡 代码覆盖率很高的情况下,与逐行调试直到找到罪魁祸首相比,通过测试找出导致 Bug 的原因要快得多

2 工具、框架和环境

2.4 集成开发环境

仓库地址:https://bitbucket.org/vfarcic/

2.6.1 Hamcrest

2.6.2 AssertJ

2.7 代码覆盖率工具

  • 不能提供质量方面的信息,只能告诉你哪些代码经过测试了
  • 代码覆盖率工具能够指出测试执行期间触及了哪些代码行,但并不能保证你遵循了良好的测试实践,因为这些指标中不包含测试质量

JaCoCo (Java Code Coverage)

1
gradle test jacocoTestReport

结果存储在目录 build/reports/jacoco/test/html

2.8 模拟框架

  • Mockito (新版已支持静态方法)
  • EasyMock
  • PowerMock
    • Mockito 和 EasyMock 可能无法提供对 static 或 final 的方法的模拟,这种情况下可以使用 PowerMock
    • 慎用:因为如果必须使用它提供的很多功能,通常昭示着设计很糟糕
    • 处理遗留代码时,PowerMock 可能是不错的选择

2.9 用户界面测试

  • Selenium
  • Selenide
    • 基于 Selenium 的项目
    • 提供了优良的测试编写语法,提高了测试的可读性
    • 它将 WebDriver 和配置隐藏,同时提供了极大的定制空间

3 红灯-绿灯-重构——从失败到成功再到完美

3.2.5 重构

  • 一旦认为可以更佳或者更优的方式重写代码,那就是重构的最佳时机
  • 最重要的规则是重构不能改变任何既有功能

3.2.6 重复

  • 经验丰富的 TDD 践行者编写 1~10 行代码后就切换到下一步,因此整个周期的持续时间为几秒~几分钟
  • 如果更长,就说明测试的范围太大,应将其分成多个更小的测试
  • 一定要快速前进快速失败并更正,然后在重复

3.4 开发“井字游戏”

只包含让测试能够通过的最少代码,而没有任何多余代码

最少:在合理范围内尽可能少

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class TicTacToeTest {

    private TicTacToe ticTacToe;

    @BeforeEach
    void before(){
        ticTacToe = new TicTacToe();
    }

    @Test
    void whenXOutsideBoardThenThrowRuntimeException(){
        assertThatThrownBy(() -> ticTacToe.play(5, 2))
                .isInstanceOf(RuntimeException.class);
    }
}
1
2
3
4
5
6
7
8
public class TicTacToe {

    public void play(int x, int y) {
        if (x < 1 || x > 3) {
            throw new RuntimeException("X is outside board");
        }
    }
}

3.4.2 需求 2

先编写测试,再编写实现代码

  • 这样做的好处是,可以确保编写的代码是可测试的,且每行代码都有对应的测试
  • 通过先编写或修改测试,开发人员可在编写代码前专注于需求
  • 这是与完成实现后再编写测试的主要差别所在
  • 测试先行的另一个好处是,可避免原本应为质量保证的测试沦为质量检查

3.4.4 需求 4

  • 通常代码编写后立即进行重构最容易也最快
  • 重构几天、几月甚至几年前编写的代码更可贵
  • 发现可让代码更好就是重构它的最佳时机

3.5 代码覆盖率

  • 使用代码覆盖率工具是一种从自己的错误中学习的极佳方式

4 单元测试——专注于当下而非过往

4.2 TDD 中的单元测试

在 TDD 中,单元测试是预先编写的,其主要目标是定义需求和设计,而验证只是副产品

5 设计——难以测试说明设计不佳

5.1 为何要关心设计

设计原则

  • 你不会需要它 YAGNI(You ain’t gonna need it)
    • 目的是消除所有冗余代码,并专注于当前而不是未来的功能
    • 代码越少,需要维护的代码就越少,同时引入 bug 的可能性也越小
  • 不要自我重复 DRY(Don’t repeat yourself)
    • 重用而不是复制以前编写的代码
    • 好处:需要维护的代码更少,并确信使用的代码是可行的
    • 好处:有助于在代码中发现新的抽象层级
  • 保持简单 KISS(Keep it simple, stupid)
    • 越简单的东西越能实现其功能
  • 奥卡姆剃刀原理
    • 它是一个这些原则,而非软件工程原则
    • 但依然适用软件工程
    • 如果你有两个或多个类似的解决方案,选择最简单的
  • SOLID
    • Robert C. Martin 发明的一个首字母缩写,涵盖 5 个面向对象编程的基本原则
    • 单一职责原则:一个类应该只有一个导致它需要修改的原因
    • 开-闭原则:最初由 Bertrand Meyer 提出
    • 里氏替换原则:由 Barbara Liskov 提出,类应该能够被扩展它的类替换
    • 接口分离原则:提供多个具体接口胜过提供单个通用接口
    • 依赖倒转原则:类应该依赖于抽象而不是实现,依赖必须专注于做什么而不是如何做

前 4 个原则是 TDD 思维的核心,因为它们皆在简化代码 最后一个原则专注于类的编写和依赖关系

5.4.9 需求 8

  • 代码覆盖率不是目标
  • TDD 的优点是:提供了很有帮助的测试,可确保未来修改方法时不会改变其行为

5.5 小结

更深入学习推荐阅读 Robert C. Martin 的两部著作

  • 《代码整洁之道》
  • 《敏捷软件开发:原则、模式与实践》

6 模拟——消除外部依赖

  • 单元测试的重点是验证单个单元是否正常,而不考虑依赖,TDD 中的单元测试尤其如此
  • 使用模拟对象
    • 代码依赖更少
    • 测试执行速度更快
  • 要快速执行测试并专注于单个功能单元,必须使用模拟对象
  • 不使用模拟对象的情况下,测试的执行速度通常很慢
  • 模拟对象非常适合用于代替:数据库、其他产品、服务等

6.2 Mockito

  • mock(): 用于创建模拟对象,还可以使用 when()giver() 指定这些模拟对象的行为
  • spy(): 可用于实现部分模拟。
    • 除非另有说明,否则间谍对象调用实际方法
    • 与模拟对象一样,对于间谍对象的每个公有或受保护的方法(静态方法除外),都可设置其行为
    • 主要差别:mock() 创建一个完全伪造的对象; spy() 使用实际对象 -** verify()**:用于检查调用方法时提供的是否是指定参数,这是一种断言

6.4.1 需求 1

💡 仅当所有测试都通过才编写新测试

  • 这样做的好处:专注于小型工作单元,而实现代码几乎始终处于能够运行的状态
  • 确保实现代码几乎始终像预期的那样工作是 TDD 的目标之一

6.5.1 分离测试

Gradle 测试所有后缀为 Spec 的类

1
2
3
4
5
6
7
test {
    include '**/*Spec.class'
}

task testInteg(type: Test) {
    include '**/*Spec.class'
}
1
gradle testInteg

6.6 小结

流行的 Java 模拟框架

  • Mockito(在平衡功能性和易用性方面是最好的)
  • EasyMock
  • JMock
  • PowerMock

7 BDD——与整个团队协作

7.2 行为驱动开发

  • BDD 是一种敏捷过程,皆在项目开发过程中始终专注于相关方的利益
  • 这是一个 TDD 变种,它也预先定义规范,更加规范完成实现并定期运行规范以验证结果
  • 不像 TDD 那样基于单元测试,BDD 倡导编写多个规范(成为场景)再开始实现(编码)

7.2.1 叙述

叙述的目标是回答如下三个基本问题:

    1. 要开发的功能的好处或价值(In order to)?
    1. 谁需要这项功能(As a)?
    1. 要开发什么样的功能(I want to)?

7.2.2 场景

关于 BDD 更广泛的用途和深入的讨论,参阅 Gojko Adzic 的著作 《实例化需求:团队如何交付正确的软件》

  • Given:定义了上下文,即场景的前置条件(例:当用户在图书页面)
  • When:定义了操作或某种事件(例:用户选择一本书;用户点击删除按钮)
  • Then:操作结果(例:图书已被删除)

7.3 书店应用程序的 BDD 故事

使用 JBehave 格式编写故事

  • 叙述都以 Narrative 行打头,后面跟着 In order to,As a,I want to
  • 列出潜在场景,都以 Scenario 开头,再做简短描述

7.4.3 Selenium 和 Selenide

PhantomJS 是一款无界面浏览器(headless browser),不使用任何 UI 就能工作

8 重构遗留代码——使其重焕青春

8.1 遗留代码

遗留代码就是不带测试的代码

Michael Feathers 在其著作《修改代码的艺术》中指出了遗留代码的一些坏味

💡 代码坏味

  • 坏味是代码中违背了基本设计元素并给设计质量带来负面影响的结构
  • 代码坏味通常不同于 bug,它们从技术上说是正确的,不会导致程序无法正常运行
  • 而是昭示着设计存在缺陷,这些缺陷可能影响开发速度或增加未来出现 bug 或故障的风险 ——摘自 https://en.wikipedia.org/wiki/Code_smell

遗留代码存在的一种场景坏味道是无法测试 一般而言,良好的设计都易于测试

💡 遗留代码困境 修改代码前,必须准备好测试;而要准备好测试,通常要修改代码

8.2.7 应用遗留代码修改算法

  • DbUnit 测试框架
  • RestAssured 测试框架

8.2.8 提取并重写调用

Roy Osherove 的著作《单元测试的艺术》

8.2.9 消除状态的“基本类型偏执”坏味

“基本类型偏执”坏味指的是使用基本数据类型表示域概念。例如:

  • 使用字符串表示消息
  • 使用整数表示金额
  • 使用结构体/字典/散列表示对象

9 功能开关——将未完成的功能部署到生产环境

9.2 功能开关

用于处理应用程序功能的优秀框架和库有很多,下面是其中的两个:

  • Togglz
  • FF4j

10 综述

10.2.2 流程

💡 仅在测试失败后才编写新代码

  • 好处:这确认了在没有实现的情况下,测试不管用
  • 如果测试不要求编写或修改实现就能通过,则说明要么它测试的功能已实现,要么测试本身存在缺陷
  • 如果测试定义的新功能没有实现而总是能够通过,就说明它毫无用处

💡 每次修改实现代码后,都再次运行所有测试

持续集成工具

  • Jenkins
  • Hudson
  • Travis
  • Bamboo

💡 仅当所有测试都通过后才编写新测试

好处:将始终专注于小型工作单元;实现代码几乎始终处于可运行的状态

💡 仅当测试都通过后才重构

  • 好处:这样的重构是安全的
  • 大多数情况下,重构期间都不需要编写新测试——对既有测试做细微修改即可

10.2.3 开发实践

💡 编写让测试能够通过的最简单的代码

好处:

  • 确保设计越来越清晰;
  • 避免实现不必要的功能
  • 遵循了 KISS 原则

💡 先编写断言,再编写操作

好处:更早澄清测试目的

💡 最大限度减少每个测试中的断言

好处:

  • 避免不知道哪个断言导致测试失败
  • 让更多断言得以执行

包含多个断言会令人迷惑,不知道测试的目标到底是什么 这种实践并不意味着每个测试方法都只能包含一个断言。如果有多个测试相同逻辑条件或功能单元的断言,可将它们都放在同一个测试方法中

💡 不要让测试依赖其他测试

好处:测试能以任何顺序独立执行,不管运行全部还是部分测试,都将如此

💡 测试的运行速度必须很快

好处:

  • 这样就能经常运行测试
  • 可快速提供反馈
  • 问题发现的越早,修复就越容易

💡 使用测试替身

好处:减少代码依赖并提高测试的执行速度

💡 Use set-up and tear-down methods

好处:让我们能够在类或各个测试方法之前和之后执行设置和拆除代码

💡 不要在测试中使用基类

好处:让测试更清晰

10.2.4 工具

💡 代码覆盖率和持续集成(CI)工具

好处:确保测试覆盖每个角落

  • JaCoCo
  • Clover
  • Cobertura

💡 结合使用 TDD 和 BDD

好处:涵盖面向开发人员的单元测试和面向客户的功能测试

comments powered by Disqus