前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、TDD

测试驱动开发(TDD)是一种软件开发方法,要求开发者在编写代码之前先编写测试用例,然后编写代码来满足测试用例,最后运行测试用例来验证代码是否正确。测试驱动开发的基本流程如下:

1、基本流程

1)编写测试用例

在编写代码之前,先根据需求编写测试用例,测试用例应该覆盖所有可能的情况,以确保代码的正确性。

2)运行测试用例

由于没有编写任何代码来满足这些测试用例,因此这些测试用例将会全部运行失败。

3)编写代码

编写代码以满足测试用例,在这个过程中,需要编写足够的代码使所有的测试用例通过。

4)运行测试用例

编写代码完成之后,运行测试用例,确保全部用例都通过。如果有任何一个测试用例失败,就需要回到第三步,修改代码,直至所有的用例都通过。

5)重构代码

在确保测试用例全部通过之后,可以对代码进行重构,例如将重复的代码抽取成函数或类,消除冗余代码等。

重构的目的是提高代码的可读性、可维护性和可扩展性。重构不改变代码的功能,只是对代码进行优化,因此重构之后的代码必须依旧能通过测试用例。

6)运行测试用例

2、TDD 场景误区

1)单元测试就是 TDD

单元测试是TDD的基础,但单元测试并不等同于TDD。

单元测试是一种测试方法,它旨在验证代码中的单个组件(例如类或方法)是否按预期工作。

TDD 是一种软件开发方法,它强调在编写代码之前先编写测试用例(即单元测试用例),并通过不断运行测试用例来指导代码的设计和实现。TDD 是基于单元测试的,TDD 的编写的测试用例就是单元测试用例。

TDD 还强调测试驱动开发过程中的重构阶段,在重构阶段优化代码结构和设计,以提高代码质量和可维护性。单元测试通常不包括重构阶段,因为它们主要关注单元组件的功能性验证

2)误把集成测试当成单元测试

TDD 在很多团队推不起来,甚至连单元测试都推不起来,归根到底是大家对 TDD 和单元测试的理解有误区。很多开发者在编写测试用例时,以为自己编写的是单元测试,但实际上写的却是集成测试的用例,原因就在于不理解单元测试和集成测试的区别。

⬤ 单元测试是指对软件中的最小可测试单元进行检查和验证的过程,通常是对代码的单个函数或方法进行测试。单元测试的对象是代码中的最小可测试单元,通常是一个函数或方法。单元测试的范围通常局限于单个函数或方法,只关注该函数或方法对输入数据的处理和输出数据的正确性,不涉及到其他函数或方法的影响,也不考虑系统的整体功能。

⬤ 集成测试是指将单元测试通过的模块组合起来进行测试,以验证它们在一起能否正常协作和运行。集成测试的对象是系统中的组件或模块,通常是多个已通过单元测试的模块组合起来进行测试。集成测试可以发现模块之间的兼容问题、数据一致性问题、系统性能问题等。

在实际开发中,许多开发者只对最顶层的方法写测试用例,例如直接对 Controller 方法编写测试用例,然后启动容器,读写外部数据库,图省事一股脑把 ControllerServiceDao 全测了。这实际上写的是集成测试的用例,这会造成:

a、测试用例职责不单一

单元测试用例职责应该单一,即只是验证业务代码的执行逻辑,不确保与外部的集成,集成了外部服务或者中间件的测试用例,都应视为集成测试。

b、测试用例粒度过大

只针对顶层的方法编写测试用例(集成测试),忽略了许多过程中的public方法,会导致单元测试覆盖率过低,代码质量得不到保障。

c、测试用例执行太慢

由于需要依赖基础设施(连接数据库),会导致测试用例执行得很慢,如果单元测试不能很快执行完成,开发者往往会失去耐心,不会再继续投入到单元测试中。 可以说,执行慢是单元测试和TDD推不起来的非常大的原因。

结论:单元测试必须屏蔽基础设施(外部服务、中间件)的调用,且单元测试仅用于验证业务逻辑是否按预期执行。

3) 项目工期紧别写单元测试了

开发者在将代码提交测试时,往往要求先自测通过才能提测。那么,自测通过的依据是什么?可以说自测通过的依据是开发者编写的单元测试用例运行通过、且覆盖了所有本次开发相关的所有核心方法。

1、在需求排期时,可以将自测的时间考虑进去,为单元测试争取足够的时间。

2、越早的单元测试作用越大,可以及早发现代码中的错误和缺陷,并及时进行修复,从而提高代码的可靠性和质量,而不是等到提测之后再修复,此时修复的成本更高。

3、在项目工期紧迫的情况下,更应该坚持写单元测试,这不会影响项目进度。相反,它可以帮助开发者提高代码的质量和可靠性,减少错误和缺陷的出现,从而避免了后期因为错误导致的额外成本和延误。

4)代码完成后再补单元测试

任何时候写单元测试都是值得鼓励的,都能使开发者从单元测试中受益。

代码完成后再写单元测试的做法会导致问题在开发过程中被忽略,并在后期被发现,从而增加了修复问题的成本和风险

TDD 要求先写测试用例再写代码,开发人员应该在编写代码前就开始编写相应的测试用例,并在每次修改代码后运行测试用例以确保代码的正确性。

5)对单元测试覆盖率的极端要求

有的团队要求单元测试覆盖率要100%,有的团队则对覆盖率没有要求。理论上单元测试应该覆盖所有代码和所有的边界条件,在实际中还需要考虑投入产出比。

⬤ 在 TDD 中,红灯阶段写的测试用例,会覆盖所有相关的 public 的方法和边界条件;

⬤ 在重构阶段,某些执行逻辑被抽取为 private 方法,开发人员要求这些 private 方法中只执行操作不再进行边界判断,因此重构后产生的private方法不需要考虑其单元测试。

6)单元测试只需要运行一次

许多开发人员认为,单元测试只要运行通过,证明自己写的代码满足本次迭代需求就可以了,之后不需要再运行。

实际上,单元测试的生命周期时和项目代码相同的,单元测试不只是运行一次,其影响会持续到项目下线。

每一次上线,都应该全量执行一遍单元测试,确保从前的测试用例都能通过,本次需求开发的代码没有影响到以前的逻辑,这样做能避免很多线上的事故。

当面对一些年代久远的系统,对内部逻辑不熟悉时,如何使变更范围可控?答案就是全量执行单元测试用例,假如从前的测试用例执行不通过了,也就意味着我们本次开发影响了线上的逻辑。

问题:老系统没有单元测试怎么办

答案:补。幸运的是现在有不少自动生成单元测试的工具,读者可以自行研究

2、单元测试覆盖率

1)pom.xml

a、root

<!-- 生成单元测试数据插件 -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M5</version>
    <configuration>
        <skip>false</skip>
        <testFailureIgnore>true</testFailureIgnore>
        <includes>
            <include>*/*Test.java</include>
        </includes>
    </configuration>
</plugin>
<!-- 生成JaCoCo覆盖率数据插件 -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.8</version>
    <executions>
        <!--  执行 prepare-agent 目标,它会启动 JaCoCo 代理 -->
        <execution>
            <id>default-prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <!-- 执行 mvn verify 时,生成测试覆盖率报告 -->
        <execution>
            <id>report</id>
            <phase>verify</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

b、web

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
    <!--  在执行 mvn verify 时,生成聚合测试覆盖率报告,所有 Maven 子模块的测试覆盖率数据 -->
    <execution>
        <id>report-aggregate</id>
        <phase>verify</phase>
        <goals>
            <goal>report-aggregate</goal>
        </goals>
    </execution>
</executions>
</plugin>

2)生成报告

做好上述报告后,直接执行 mvn test 就可以生成单测覆盖率报告了。如果没有什么异常的话,程序会生成单测覆盖率报告文件,地址为: target/site/jacoco/index.html

a、报告1

clean org.jacoco:jacoco-maven-plugin:prepare-agent package -DskipTests=false -Dmaven.test.failure.ignore=true  org.jacoco:jacoco-maven-plugin:report surefire-report:report -Ptest

image-20230714174333359

b、报告2

mvn test

image-20230714174631411

元素  
Element  
Missed Instructions 代码指令 统计图
Missed Instructions Cov 代码指令 覆盖率
Missed Branches 分支 覆盖 统计图
Missed Branches Cov 分支 覆盖率
Missed 圈复杂度 (循环)未覆盖数
Cxty 圈复杂度 (循环) 总数
Missed 行 未覆盖数
Lines 行总数
Missed 方法 未覆盖数
Methods 方法总数
Missed 类 未覆盖数
Classes 类总数

二、契约测试

契约测试是一种在软件开发中用于验证不同组件或系统之间交互是否符合约定的测试方法

1、定义与概念

  • 契约测试主要关注服务的提供者和消费者之间的契约或约定。它确保服务的提供者按照约定的格式、规则和行为提供服务,同时消费者也按照约定来使用服务,双方在交互过程中都能满足彼此的期望,从而保证系统的稳定性和可靠性。

2、工作原理

  • 契约测试通过模拟服务的提供者和消费者来验证契约。在测试过程中,会为服务的消费者创建一个虚拟的提供者,称为 “桩(stub)” 或 “模拟对象(mock)”,它模拟了真实服务提供者的行为和响应。同样,也会为服务的提供者创建一个虚拟的消费者来发送请求并验证响应。通过这种方式,检查双方在数据格式、操作流程、响应状态等方面是否符合事先约定的契约

3、测试内容

  • 请求与响应验证:检查消费者发送的请求是否符合提供者所期望的格式、参数类型和取值范围等,以及提供者返回的响应是否与消费者所期望的结构、数据类型和内容一致。
  • 边界条件测试:针对契约中的边界情况进行测试,例如请求参数的最大值、最小值、空值等情况,验证提供者和消费者在这些边界条件下是否能正确处理。
  • 行为验证:不仅仅关注数据的交换,还包括对服务调用的顺序、频率等行为的验证。例如,某些操作是否只能在特定条件下执行,或者某些接口是否按照约定的顺序被调用。

3、实施步骤

  • 契约定义:首先,服务的提供者和消费者需要共同定义契约,明确双方的交互规则,包括请求和响应的格式、数据类型、操作方法等,可以使用诸如 OpenAPI、JSON Schema 等规范来定义契约。
  • 测试编写:根据契约定义,为服务的消费者和提供者编写测试用例。消费者端的测试主要验证其发送的请求是否符合契约以及对提供者响应的处理是否正确。提供者端的测试则侧重于验证其对各种合法和非法请求的处理是否符合契约规定。
  • 测试执行:在隔离的环境中分别执行消费者和提供者的契约测试,确保双方在独立运行时都能满足契约要求。然后,进行集成测试,将消费者和提供者进行实际的集成,再次验证它们之间的交互是否仍然符合契约。
  • 结果验证与反馈:检查测试结果,查看是否存在违反契约的情况。如果发现问题,及时反馈给相关开发人员,以便进行修复和改进。

4、应用场景

  • 微服务架构:在微服务架构中,各个微服务之间通过接口进行通信,契约测试可以确保不同微服务之间的交互稳定,即使某个微服务进行了升级或修改,只要契约不变,就不会影响到其他微服务的正常运行。
  • 第三方服务集成:当应用程序需要集成第三方服务时,通过契约测试可以保证与第三方服务的集成是可靠的,双方都按照约定的方式进行数据交互和操作。
  • 前后端分离开发:在前后端分离的项目中,前端和后端通过接口进行数据交互,契约测试可以确保前端发送的请求能够被后端正确处理,后端返回的数据也能被前端正确解析和展示。

契约测试有助于提高软件系统的可维护性、可扩展性和稳定性,减少因接口变更或交互不规范而导致的问题,提高软件质量和开发效率。

ContactAuthor