单元测试最佳实践

原则

  • [F]AST 快速性
  • [I]solate 隔离性
  • [R]epeatabel 可重复性
  • [S]elf-Validating 自验证性
  • [T]imely 及时性

Fast 快速

单元测试应该尽量保持简单和快速,应该做到毫秒级

需要考虑输入数据的大小、数量和质量对代码性能的影响,并对测试用例的执行时间进行评估。

  • 采用内存数据库
  • 依赖的外部接口,采用 Mock 的方式进行测试

Isolate 隔离

测试用例应该是独立的,不受其他测试用例的影响,不依赖其他单元测试的执行顺序,不依赖外部资源。

  • 对于依赖的外部接口,外部存储都是用 Mock 解决
  • 对于 DI 框架中所依赖的类,直接注入到内存
  • 一个单元测试应该关注与逻辑的一个方面(存在多个断言应思考是否真正需要,真正需要是否抽离新的测试用例方法)

多关注单测的隔离性,同时也会驱动类、方法实现单一职责(SRP)

Repeatabel 可重复性

每次都是重复的入参和结果,运行都是固定不变的

不受外界的影响,如:

  • 时间,经典的例子就是判断时间的业务中,某些时间运行成功,某些时间运行失败

Self-Validating 自验证性

必须使用断言 Assert 方式来进行正确性验证,而不是 var_dump、System.out、Println 等输出进行人肉校验。

单测必须走持续集成&自动化测试,而不是人工上线运行。

Timely 及时性

单元测试应该具有及时性,先于开发而不是后续开发完在补充。

如何限制后续补充单元测试的坏习惯:

  • 代码 review 时查看
  • git 提交阶段或服务部署阶段使用工具限制

除了 FIRST 原则还有 AIR 原则、ASCII 原则等,具体都没啥大区别,可参考那些年,我们写过的无效单元测试_阿里云云栖号的博客-CSDN 博客

其他准则

BCDE 原则,以保证被测试模块的交付质量。

  • B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。

  • C:Correct,正确的输入,并得到预期的结果。

  • D:Design,与设计文档相结合,来编写单元测试。

  • E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。

  • 合理的命名测试用例:确保每个方法只测试 "被测类" 的一个明确特性, 并相应的命名测试方法. 典型的命名俗定是 test[what], 比如 testSaveAs(), testAddListener(), testDeleteProperty()

  • 避免过度断言:永不失败的测试没有价值,一个测试应该只有一个失败的原因。

  • 数据操作有完整流程,且数据有标记:不应该手动在数据库插入数据,而是由程序插入或者导入数据的方式来准备数据,并且自动回滚机制,不给数据库造成脏数据,如无法避免产生的数据有明确的前后缀标识。

用例结构

一般都是一个三段式的用例结构,目前较常见的就是 AAA 和 GWT,整体测试结构时相同的。如果说两者有什么区别的话,那区别在于 AAA 的构思更倾向于技术实现,GWT 更倾向于业务流程。

与其说 AAA 与 GWT 区别,不如说 TDD(Test-Driven Development) 和 BDD(Behavior-Driven Development),区别。

AAA

Arrange-Act-Assert 是单元测试时的常见模式。顾名思义,它由三个主要操作组成:

  • Arrange:准备测试环境,包括设置对象的状态、参数、依赖项等。
  • Act:执行测试代码,即调用要测试的方法或代码块。
  • Assert:验证测试结果是否符合预期,即判断输出结果是否正确或符合要求。

例如,对于以下方法的测试:

public int add(int x, int y) {
    return x + y;
}

使用 AAA 方法的测试代码如下:

@Test
public void testAdd() {
    // Arrange
    int x = 3;
    int y = 4;
    MyClass myClass = new MyClass();

    // Act
    int result = myClass.add(x, y);

    // Assert
    assertEquals(7, result);
}

GWT

Given-When-Then

  • Given:设置测试环境,包括设置对象的状态、参数、依赖项等。
  • When:执行测试代码,即调用要测试的方法或代码块。
  • Then:验证测试结果是否符合预期,即判断输出结果是否正确或符合要求。

例如,对于以下方法的测试:

public String greeting(String name) {
    if (name == null) {
        throw new IllegalArgumentException("Name cannot be null");
    }
    return "Hello, " + name + "!";
}

使用 GWT 方法的测试代码如下:

@Test
public void testGreeting() {
    // Given 准备用例所需的上下文
    String name = "Alice";
    MyClass myClass = new MyClass();

    // When 调用待测的函数
    String result = myClass.greeting(name);

    // Then 断言
    assertEquals("Hello, Alice!", result);
}

@Test(expected = IllegalArgumentException.class)
public void testGreetingWithNullName() {
    // Given
    String name = null;
    MyClass myClass = new MyClass();

    // When
    myClass.greeting(name);

    // Then (expecting exception)
}

参考

二、优秀单元测试的五个特征 FIRST - 纪玉奇 - 博客园
单元测试准则
要对自己写的代码负责 | 蓝士钦
有效的单元测试之一--代码坏味道(读书笔记精华带源码)
unit-test-specification/README.md at master · cyneck/unit-test-specification · GitHub

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容