1 概述
在还没有写什么代码之前,就引入单元测试,是要强调单元测试的重要性。当一套代码的生命周期比较长的时候,单元测试更加重要。生命周期长的代码,不管是产品人员还是开发人员,可能都会换了一批又一批,维护才是成本的大头。后来的人缺乏这套代码的历史知识,改起来就会bug频出,质量堪忧。如果有单元测试代码,则每次的修改如果有问题,就能够通过单元测试代码暴露出来,这样改代码会比较让人放心。但单元测试代码是一项做了则太平无事看不出作用的事情,但不做则经常要灭火而追悔莫及的事情。如果公司不强制要求或者工期比较紧张的,很可能是被忽略的,等到代码积累多了、历史知识又丢失了不少的时候,再来想补,就困难重重了。
所以,在此要先引入单元测试的基础设施,并在写代码的时候都加上单元测试,以达到编写代码达到100%的测试覆盖率。这个“100%”如果变成什么“80%”之类的,效果就会差很多,因为哪些测了哪些没测就分不清楚了,就很难监督单元测试的执行了。如果实在有些是测不了的,那么应该在工具中排除掉,排除掉的代码应该尽量少,在此基础上保持“100%”的覆盖率,使得能够比较好的监督单元测试有没有在正常执行。
2 单元测试
2.1 引入依赖
Springboot已经对测试给出了自己的推荐,也是目前比较流行的选择,如果没有什么特殊需求的话,直接选用即可。
首先,在<dependencies>中引入test的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
就是这样一个依赖,就基本足够了。
2.2 具体依赖
找到 https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-test/2.7.18/spring-boot-starter-test-2.7.18.pom 文件,看看里面的具体依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.18</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.7.18</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<version>2.7.18</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.22.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.5.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.5.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
上面是一些主要依赖的摘录,前三个是引入springboot的基础,单元测试库junit已经是事实标准,通过junit-jupiter一起引入;断言库同时引入了assertj和hamcrest,它们各有各的优势,这里优先使用assertj;mock库则选用了mockito。mock对象,进行单元测试,最后断言结果,这是测试的三个步骤,分别使用了对应的库,这几种库的语法需要学一学。
单元测试的代码要求逻辑简单,就做上面三件事情,不要有什么业务逻辑,单元测试之间也不要有什么关系,保证单元测试代码简单,因为没有人再来测试单元测试代码了。
3 测试覆盖率
单元测试是一个做了不好看到效果,如果没有限制,那做多多少全凭开发的心情,那大概率很快就会荒废掉。所以需要有监督方式,其中就是引入测试覆盖率工具,这里选用JaCoCo,通过它来生成测试覆盖率,每次写完代码都应该检查测试覆盖率,确保单元测试覆盖所有预期的代码。
3.1 工具引入
引入JaCoCo,只需要在pom.xml中的里增加插件,版本根据情况选比较新的:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
</plugin>
3.2 生成测试覆盖率
3.2.1 IDEA生成测试覆盖率
在IDEA中,选中单元测试用例类(可以整个目录),然后右键选择菜单“Run Tests in project_name with Coverage”:
IDEA会执行对应的测试用例,如果是在测试的根目录执行,则可以得到所有测试用例的测试覆盖率,并把结果展示到一个“Coverage”窗口里:
针对那些覆盖率不到100%的类进行检查和补充测试用例,如果要看详情则可以导出报告:
导出的报告里有个index.html,打开该文件会展示各个类的测试覆盖率,选择覆盖率不到100%的类,里面是绿色的则是覆盖的,红色的则是未覆盖的,针对红色部分的代码进行补充测试用例。
3.2.2 命令行生成测试覆盖率
IDEA会在底下默默做一些事情,让操作比较简单,这对开发人员比较友好。但如果想在持续集成工具如Jenkins上也能够生成测试覆盖率,那么还需要增加点配置:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory/}coverage_reports</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
上面<plugin>配置增加了一个<execution>,可以通过命令mvn clean test在执行测试用例的时候就把覆盖率报告生成到coverage_reports目录下,该目录可以根据需要进行修改。
3.2.3 覆盖率100%
作为开发人员,覆盖率就应该追求覆盖率100%。在制度上也应该这样设定,这样比较容易监督。实在无法测试或者没有必要测试的则可以排除掉,排除的类也可以定期进行检查,看看是否真的符合排除的条件。这个需要在工具上能够检测,使用mvn jacoco:check命令进行检测:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.13</version>
<configuration>
<excludes>
<exclude>com/qqian/stepfmk/srvpro/SrvproApplication.class</exclude>
</excludes>
<rules>
<rule>
<element>PACKAGE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>1</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory/}coverage_reports</outputDirectory>
</configuration>
</execution>
<execution>
<id>check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
上面配置中增加了check的<execution>节点,同时在这个插件的根里增加了配置:
1、排除掉一部分无法测试的类(可以用*、**来模糊匹配),如springboo的启动类没有必要进行测试;
2、增加<rules>节点指定按行(LINE)检查覆盖率,要达到100%。规则的配置可以参考:https://www.jacoco.org/jacoco/trunk/doc/check-mojo.html
3.2.4 黑盒测试覆盖率
公司一般还有专门的测试部门,他们做的主要是黑盒测试。Jacoco也支持在java程序在运行的过程中收集测试覆盖率信息,这样就可以了解到黑盒测试到底覆盖了哪些代码,哪些没有被覆盖到。
在启动Java程序的时候,增加一个agent服务器,用于后面dump测试覆盖率的report:
java -jar srvpro-1.0.0-SNAPSHOT.jar -javaagent:jacocoagent.jar=excludes=com/qqian/stepfmk/srvpro/SrvproApplication.class,output=tcpserver,address=127.0.0.1,port=6300
-javaagent:jacocoagent.jar的格式为:-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2],即其value是一个key-value对形式,用逗号分隔,key和value用等号分隔。更详细可参考:https://www.eclemma.org/jacoco/trunk/doc/agent.html
jacocoagent.jar包下载:https://www.eclemma.org/jacoco/index.html
执行测试后,需要用cli命令先dump出数据,然后用report命令把数据转为hmtl文件才可以看:参考https://www.jacoco.org/jacoco/trunk/doc/cli.html
# dump数据
java -jar jacococli.jar dump [--address <address>] --destfile <path> [--help] [--port <port>] [--quiet] [--reset] [--retry <count>]
例子:java -jar jacococli.jar dump --address 127.0.0.1 --port 6300 --destfile /path/jacoco.exec
# 转成report
java -jar jacococli.jar report [<execfiles> ...] --classfiles <path> [--csv <file>] [--encoding <charset>] [--help] [--html <dir>] [--name <name>] [--quiet] [--sourcefiles <path>] [--tabwith <n>] [--xml <file>]
例子:java -jar jacococli.jar report /path/jacoco.exec
4 架构一小步
1、通过spring-boot-starter-test引入测试库junit、断言库assertj、mock库mockito;
2、规范:把单元测试放到比较重要的位置,测试覆盖率要达100%;
3、规范:单元测试代码只做mock对象、测试业务逻辑、断言三件事,测试代码要保持简单,不能有业务逻辑。