文章目录
一、目标
- 学会基于AssertJ的断言技术;
- 学会基于AssertJ-DB的数据库断言技术;
- 学会基于JMockit的mock技术;
- 学会内存和数据库的造数;
- 学会集成Maven进行单元测试、集成测试的执行;
- 学会查看测试覆盖率;
二、断言技术
断言库包含很多,比如junit自带的、hamcrest等,这里推荐使用AssertJ,看它的官网就知道了,宣称
fluent assertions java library
。
2.1 核心库断言
AssertJ的断言采用
assertThat(result)
的形式,等同于
then(result)
,这两种方式使用上没有区别;我们需要在pom中引入如下依赖:
<dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.15.0</version><scope>test</scope></dependency>
下面列出常见的断言用法,其它的用法可以参考依赖库学习使用。
- 对文本的断言;
assertThat(result).isEqualTo("apple");assertThat(result).isEqualToIgnoringCase("apple");assertThat(result).contains("apple");assertThat(result).containsIgnoringCase("apple");assertThat(result).startsWith("apple");assertThat(result).matches("^[A-Za-z0-9]{8}$");assertThat(result).hasSize(10);assertThat(result).containsSequence("a","p","l");...
- 对数字的断言;
assertThat(result).isGreaterThanOrEqualTo(100);assertThat(result).isCloseTo(100.0,Offset.offset(0.000001));assertThat(result).isBetween(90.0,91.0),assertThat(result).isNaN();...
- 对日期的断言;
assertThat(result).isAfter(startDate);assertThat(result).isBefore("2020-01-01");assertThat(result).isInSameMonthAs("2019-12-01");...
- 对集合的断言;
assertThat(result).hasSize(3);assertThat(result).contains("apple","orange");assertThat(result).doesNotcontain("apple","orange");assertThat(result).containsExactly("apple","orange");assertThat(result).startsWith("apple");assertThat(result).endsWith("orange");assertThat(result).doesNotContainNull();assertThat(result).doesNotHaveDuplicates();assertThat(result).isNotEmpty();assertThat(result).isNullOrEmpty();...assertThat(result).hasSize(2);assertThat(result).containsEntry("apple","12");assertThat(result).containsKeys("apple","orange");assertThat(result).containsOnlyKeys("apple","orange");assertThat(result).containsValues("apple","orange");...
- 对对象的断言;
assertThat(result).isEqualToComparingOnlyGivenFields(obj1,"name","weight");assertThat(result).isEqualToIgnoringGivenFields(obj1,"name","weight");assertThat(result).isEqualToIgnoringNullFields(obj1);...
2.2 数据库断言
AssertJ-Core只适合为单元测试使用,如果要进行集成测试,或者只测试DAO层的SQL执行结果,就无能为力了,这是就需要用到AssertJ-DB,首先我们需要在pom中引入如下的依赖:
<dependency><groupId>org.assertj</groupId><artifactId>assertj-db</artifactId><version>1.3.0</version><scope>test</scope></dependency>
下面是一些常用的功能:
- 数据源如果我们想使用SpringBoot项目中配置的数据源,比如在
application.properties
中的数据库配置项:spring.datasource.url=jdbc:postgresql://localhost:5432/mydb?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=truespring.datasource.username=postgrespring.datasource.password=postgrespring.datasource.driver-class-name=org.postgresql.Driver
那么我们就需要在运行该单元测试的时候启动整个Spring Boot工程,首先需要先建立一个测试基类:@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes =DailyWorkServerApplication.class, webEnvironment =SpringBootTest.WebEnvironment.DEFINED_PORT)@Transactional@RollbackpublicclassBaseTest{// 集成测试基类// 如果使用maven运行测试用例,需要在maven-surefire-plugin插件中将本基类排除执行,否则会报错,因为没有测试用例}
然后,我们的测试基类继承该测试基类:publicclassSystemInfoDaoTestextendsBaseTest{// 获取系统数据源@AutowiredprivateDataSource dataSource;@AutowiredprivatePersonDao personDao;@TestpublicvoidgetPersonCount(){// 构造一个连接到数据源的Request,此处可以先略过,后面会有详细介绍Request request =newRequest(dataSource,"select count(1) from person where name = ?","zhangsan");// assertj-db执行如上Request中的SQL,对获取的数据进行断言assertThat(request).row(0).column().value().isEqualTo(1);}}
如果你不想使用SpringBoot的数据源,需要自定义数据源,那么可以在测试类中这么写:publicclassSystemInfoDaoTestextendsBaseTest{privateSource dataSource;@AutowiredprivatePersonDao personDao;privatestaticfinalString DB_URL ="jdbc:postgresql://localhost:5432/mydb?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true";privatestaticfinalString DB_USER_NAME ="postgre";privatestaticfinalString DB_PASSWORD ="postgre";@Beforepublicvoidbefore(){this.dataSource =newSource(DB_URL,DB_USER_NAME,DB_PASSWORD);}@TestpublicvoidgetUmCount(){// 构造一个连接到数据源的Request,此处可以先略过,后面会有详细介绍Request request =newRequest(dataSource,"select count(1) from person where name = ?","zhangsan");// assertj-db执行如上Request中的SQL,对获取的数据进行断言assertThat(request).row(0).column().value().isEqualTo(1);}}
当然,还可以使用其它类型的DataSource,详细信息可以参考文末关于AssertJ-DB的官网内容。 - Table当数据源连接上之后,我们可以使用如下的语句来代表某一张具体的表:
Table table =newTable(dateSource,"person");
- Request一个Request可以代表一个即将要执行的SQL请求:
Request request =newRequest(dataSource,"select count(1) from person where name = ?","zhangsan");
- RowRow是基于上面table和request的结果的某一行数据:
// 取当前表的第二行数据table.row(1);// 取当前请求的第4行数据,然后再跳到第11行数据request.row(3).row(10);
- ColumnColumn是基于上面table和request的结果的某一列数据:
// 取当前表的第二列数据table.column(1);// 取当前请求的第4列数据,然后再跳到第11列数据request.column(3).column(10);// 取当前请求的第2行数据,然后取当前行的第4列单元格request.row(1).column(3);
- ValueValue是基于Row或者Column的某一单元格中的值:
// 取当前请求的第2行数据,然后取当前行的第4列单元格的值request.row(1).column(3).value();
总结下来,只有DAO层的对数据库的增、删、改操作才需要使用AssertJ-DB,而查询操作是不需要的,因为查询已经将数据加载到内存中,只要使用AssertJ-Core做断言比较即可。关于这些常用功能的详细案例,可以参考文末的Assertj-DB文档。
PS:
实验表明,对于事务回滚控制的测试用例,assertJ-DB似乎并不能得到我们想要的结果。
如下案例中,测试用例是事务回滚的,但是使用JdbcTemplate可以得到正确的结果,但是使用assertJ-DB就不行了。只能针对非事务回滚的测试用例,assertJ-DB才能得到正确的结果。这个目前还不知道怎么解决,暂时只能用JdbcTemplate替代。
@TestpublicvoidaddSystemInfoTest(){SystemUpdateDTO systemUpdateDTO =newSystemUpdateDTO();
systemUpdateDTO.setSysNameCN("测试-商品管理系统");
systemUpdateDTO.setSysNameEN("test-GMS");// 测试DAO逻辑-插入一条数据
systemInfoDao.addSystemInfo(systemUpdateDTO);String querySql ="select count(1) from dw_sys_info dsi";JdbcTemplate jdbcTemplate =newJdbcTemplate(dataSource);Integer rows = jdbcTemplate.queryForObject(querySql,Integer.class);// 1System.out.println("总共有"+ rows);Request request =newRequest(dataSource, querySql);// 0System.out.println("总共有"+ request.getRow(0).getColumnValue(0).getValue());}
三、Mock技术
Mock框架有很多,古老的JMock、社区活跃的Mockito、还有我们今天要介绍的主角JMockit。
Mock技术是为了隔离被测试方法依赖的外部变量,从而可以使得测试方法的表现只受被测试方法本身的逻辑影响。举个例子:
@Service("personService")publicclassPersonServiceImplimplementsPersonService{@AutowiredprivateInvokeService invokeService;@OverridepublicIntegergetPersonCountBySchool(String school){if(StringUtils.isEmpty(school)){return0;}// 调用关联方获取数据的数量return invokeService.getPersonBySchool(school).size();}...}
我们如果想测试
getPersonCountBySchool
能否正常返回数据的数量,我们不必真的去执行
invokeService.getPersonBySchool(school)
调用关联方,只要使用Mock技术,让其返回我们设定的值即可:
publicclassPersonServiceImplTestextendsBaseTest{@Tested@AutowiredprivatePersonService personService;@TestpublicvoidtestGetPersonCountBySchool(@InjectableInvokeService invokeService){// 准备数据List<Person> personList =newArrayList<>();Person peter =newPerson("东方高中");Person jack =newPerson("东方高中");
personList.add(peter);
personList.add(jack);// 模拟录制newExpectations(){{// 模拟调用关联方获取数据列表,无论入参是什么字符串,都返回上面准备好的列表
invokeService.getPersonBySchool(anyString);
result = personList;}};// 重放Integer personCount = personService.getPersonCountBySchool("华夏高中");// 验证assertThat(personCount).isEqualTo(2);}}
在这里,最重要的两个注解就是@Tested和@Injectable,前者代表需要测试的类,后者代表需要mock的对象。
JMockit支持mock一个类、mock一个对象实例、mock一个对象中的某个具体的方法,甚至还可以对传入的参数进行检查,更多细节请参考文末列举的JMockit的官方文档。
四、造数技术
4.1 内存中造数
我们在运行单元测试的时候,为了满足调用参数的要求,不得不为参数对象设置值。比如,当参数对象为一个Person类的时候,倘若它的属性值不多,我们可以像上面的例子中一样,使用手动造数;但是,如果属性值很多,甚至中间还嵌套了其它对象怎么办?手动造数太繁琐了。
- java-faker,可以对生活中常用的事物进行造数,使用简单,但无法满足复杂对象的造数;
- easy-random,可以对复杂对象进行造数,而且可以自定义造数的值类型和范围;
- jmockdata,可以对复杂对象进行造数,而且可以自定义造数的值类型和范围;
这些工具库的使用都非常简单,参考文末列出的官方文档看下即可。
4.2 数据库造数
我们在测试DAO层关于SQL的增删查改前,要先提供一批专供测试使用的假数据,一般有以下方式:
- 使用内存数据库如果不希望测试用例的执行污染测试数据库,那么可以建立一个专为测试用例执行使用的内存数据库,缺点是,需要维护所有的建表语句。
- 使用数据库造数工具可以使用DBFactory之类的造数工具,往测试数据库中提前准备数据,但是测试完成后删除数据是个问题。
- 测试用例使用事务回滚好处是不会对测试数据库造成数据污染,但是需要在测试用例逻辑执行前,手动准备数据;
五、Maven集成
我们在如上的学习过程中,都是写完单元测试后直接运行了。倘若我们在提交代码前,要运行所有的单元测试该怎么操作呢?总不可能一个个地打开所有地测试类,都点击运行一遍吧。
这里介绍使用Maven的插件进行单元测试运行的集成操作。
5.1 默认配置
首先,在pom文件中引入
maven-surefire-plugin
,版本选择最新版:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.19</version></plugin>
其实,这就可以了,我们使用了默认配置,执行mvn生命周期的test,就可以运行
src/test/java
目录下的所有单元测试和集成测试了。
5.2 跳过执行测试用例
有时候我们编译工程并不像运行测试用例,那么可以增加如下配置:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.19</version><configuration><!--跳过执行测试用例--><skipTests>true</skipTests></configuration></plugin>
5.3 选定运行测试用例
有些场景下,我们只想运行某一个/一类/一路径的测试用例,我们可以使用
<include>
来配置:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.19</version><configuration><!--只运行包含的测试--><includes><include>unit/**/*.java</include></includes></configuration></plugin>
如上配置表示,只运行
src/test/java/unit
路径下所有java结尾的测试类中的测试用例;
5.4 排除运行测试用例
有时候,我们需要排除运行一些测试用例。比如,在编译阶段,我们只想快速地运行单元测试,而不要运行集成测试,那么就可以将集成测试所在的文件路径做如下的配置:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.19</version><configuration><!--需要排除的测试--><excludes><!--当不需要运行集成测试时添加如下文件内容--><exclude>integration/**/*.java</exclude></excludes><!--设置并发执行测试,节约时间--><parallel>methods</parallel><threadCount>10</threadCount></configuration></plugin>
前提是,我们所有的集成测试类都必须放在
src/test/java/integration
目录下。
5.5 多线程运行测试用例
有时候,项目中的单元测试和集成测试非常多,一次执行会耗时比较久,那么可以设置多线程来执行:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.19</version><configuration><!--设置并发执行测试,节约时间--><parallel>methods</parallel><threadCount>10</threadCount></configuration>
需要注意的时,要确保各测试用例之间没有调用依赖,否则便不可使用多线程的方式。
5.6 测试报告及覆盖率的查看
如果仅靠上面引入的
maven-surefire-plugin
插件,那么你只能在控制台看到运行的测试报告,如果要跟别人分享,十分不方便。可以通过引入
maven-surefire-report-plugin
插件:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-report-plugin</artifactId><version>2.12.2</version><configuration><!--报告中是否显示成功率为100%的项目--><showSuccess>false</showSuccess></configuration></plugin>
执行其中的
surefire-report:report
命令,就可以重新运行所有测试用例,并在
target/site
目录下生成一个html测试报告。
如果想查看测试覆盖率,那么就需要引入另外一个插件
cobertura-maven-plugin
:
<plugin><groupId>org.codehaus.mojo</groupId><artifactId>cobertura-maven-plugin</artifactId><version>2.5.1</version></plugin>
执行其中的
cobertura:cobertura
命令,就可以重新运行所有测试用例,并在
target/site/cobertura
目录下生成一个静态站点文件,找到其中的
index.html
,打开就可以看到各个测试覆盖率数据了。
5.7 其它配置
关于
maven-surefire-plugin
插件还有很多其它配置内容,可以参考文末引用自行阅读尝试。
六、经验总结
- 测试用例的名称一定要突显被测试代码的意图,名称不一定要以“Test”结尾,可以很长,单词之间用下划线连接;
- 要注重测试用例代码的可读性,让人一眼就能看出测试意图;
- 测试用例中应该避免使用分支和循环,可以拆成多个测试用例;
- 每个测试用例使用prepare-action-verify三段式结构;
- 不要在测试用例中捕获异常,应该抛出异常或者期待异常
@Test(expected=SomeException)
,当然还可以使用ExpectedException; - 测试用例不能依赖数据库中的已有数据,应该在测试用例中自己准备数据;
- 测试完成后应该回滚数据,避免造成数据库污染,保证测试用例可以反复执行;
- 通常不使用单元测试来测JavaBean和Controller,所以这两者中尽量也不要有业务逻辑;
七、参考文献
- AssertJ Core
- AssertJ-DB
- JMockit中文网
- java-faker
- easy-random
- jmockdata
- 学习Maven之Maven Surefire Plugin(JUnit篇)
- maven-surefire-plugin
版权归原作者 掂掂三生有幸 所有, 如有侵权,请联系我们删除。