正文
一、目标
学会基于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=true spring.datasource.username=postgre spring.datasource.password=postgre spring.datasource.driver-class-name=org.postgresql.Driver
那么我们就需要在运行该单元测试的时候启动整个Spring Boot工程,首先需要先建立一个测试基类:
@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = DailyWorkServerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @Transactional @Rollback public class BaseTest { // 集成测试基类 // 如果使用maven运行测试用例,需要在maven-surefire-plugin插件中将本基类排除执行,否则会报错,因为没有测试用例 }
然后,我们的测试基类继承该测试基类:
public class SystemInfoDaoTest extends BaseTest { // 获取系统数据源 @Autowired private DataSource dataSource; @Autowired private PersonDao personDao; @Test public void getPersonCount() { // 构造一个连接到数据源的Request,此处可以先略过,后面会有详细介绍 Request request = new Request(dataSource, "select count(1) from person where name = ?", "zhangsan"); // assertj-db执行如上Request中的SQL,对获取的数据进行断言 assertThat(request).row(0).column().value().isEqualTo(1); } }
如果你不想使用SpringBoot的数据源,需要自定义数据源,那么可以在测试类中这么写:
public class SystemInfoDaoTest extends BaseTest { private Source dataSource; @Autowired private PersonDao personDao; private static final String DB_URL = "jdbc:postgresql://localhost:5432/mydb?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true"; private static final String DB_USER_NAME = "postgre"; private static final String DB_PASSWORD = "postgre"; @Before public void before(){ this.dataSource = new Source(DB_URL,DB_USER_NAME,DB_PASSWORD); } @Test public void getUmCount() { // 构造一个连接到数据源的Request,此处可以先略过,后面会有详细介绍 Request request = new Request(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 = new Table(dateSource, "person");
Request
一个Request可以代表一个即将要执行的SQL请求:
Request request = new Request(dataSource, "select count(1) from person where name = ?", "zhangsan");
Row
Row是基于上面table和request的结果的某一行数据:
// 取当前表的第二行数据 table.row(1); // 取当前请求的第4行数据,然后再跳到第11行数据 request.row(3).row(10);
Column
Column是基于上面table和request的结果的某一列数据:
// 取当前表的第二列数据 table.column(1); // 取当前请求的第4列数据,然后再跳到第11列数据 request.column(3).column(10); // 取当前请求的第2行数据,然后取当前行的第4列单元格 request.row(1).column(3);
Value
Value是基于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替代。
@Test public void addSystemInfoTest(){ SystemUpdateDTO systemUpdateDTO = new SystemUpdateDTO(); systemUpdateDTO.setSysNameCN("测试-商品管理系统"); systemUpdateDTO.setSysNameEN("test-GMS"); // 测试DAO逻辑-插入一条数据 systemInfoDao.addSystemInfo(systemUpdateDTO); String querySql = "select count(1) from dw_sys_info dsi"; JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); Integer rows = jdbcTemplate.queryForObject(querySql,Integer.class); // 1 System.out.println("总共有" + rows); Request request = new Request(dataSource, querySql); // 0 System.out.println("总共有" + request.getRow(0).getColumnValue(0).getValue()); }
三、Mock技术
Mock框架有很多,古老的JMock、社区活跃的Mockito、还有我们今天要介绍的主角JMockit。
Mock技术是为了隔离被测试方法依赖的外部变量,从而可以使得测试方法的表现只受被测试方法本身的逻辑影响。举个例子:
@Service("personService") public class PersonServiceImpl implements PersonService{ @Autowired private InvokeService invokeService; @Override public Integer getPersonCountBySchool(String school){ if(StringUtils.isEmpty(school)){ return 0; } // 调用关联方获取数据的数量 return invokeService.getPersonBySchool(school).size(); } ... }
我们如果想测试getPersonCountBySchool能否正常返回数据的数量,我们不必真的去执行invokeService.getPersonBySchool(school)调用关联方,只要使用Mock技术,让其返回我们设定的值即可:
public class PersonServiceImplTest extends BaseTest { @Tested @Autowired private PersonService personService; @Test public void testGetPersonCountBySchool(@Injectable InvokeService invokeService) { // 准备数据 List<Person> personList = new ArrayList<>(); Person peter = new Person("东方高中"); Person jack = new Person("东方高中"); personList.add(peter); personList.add(jack); // 模拟录制 new Expectations(){ { // 模拟调用关联方获取数据列表,无论入参是什么字符串,都返回上面准备好的列表 invokeService.getPersonBySchool(anyString); result = personList; } }; // 重放 Integer personCount = personService.getPersonCountBySchool("华夏高中"); // 验证 assertThat(personCount).isEqualTo(2); } }
在这里,最重要的两个注解就是@Tested和@Injectable,前者代表需要测试的类,后者代表需要mock的对象。
JMockit支持mock一个类、mock一个对象实例、mock一个对象中的某个具体的方法,甚至还可以对传入的参数进行检查,更多细节请参考文末列举的JMockit的官方文档。