前言
Dao层的单元测试,测试目的在于:
- 验证数据连接环境(包括ORM的配置和Dao的实现)的正确性。
- 验证sql语句的正确性。
单元测试的常规步骤是:
- 准备测试数据。
- 执行待测方法。
- 检查执行结果。
#1通常需要初始化db数据,#3通常也是检查数据库结果。
在每个开发人员使用独立数据库的前提下,DbUnit提供了一套控制数据库状态的方法,简化#1和#3的工作。而Spring Test DbUnit则提供了Spring和DbUnit集成,允许开发人员使用annotation而不是继承来完成数据库的setup和teardown。
待测项目介绍
整个测试基于todo-list,个人为巩固Spring MVC + MyBatis框架搭建的练习项目。
该项目的数据库只有一张数据表(tbl_todos),结构如下:
Field |
Type |
Comments |
id |
bigint (20) |
自增主键 |
title |
varchar(50) |
|
description |
varchar(500) |
|
status |
tinyint(1) |
0:未完成 1:已完成 |
Dao接口包含几个最常见的CRUD方法,结构如下:
1
2
3
4
5
6
7
|
public interface TodoDao {
int insert(TodoItem todoItem);
int update(TodoItem todoItem);
int deleteByPrimaryKey(Long id);
TodoItem selectByPrimaryKey(Long id);
List<TodoItem> selectAll();
}
|
测试用例设计
默认CRUD的测试用例比较简单,也不是本文重点,略。
测试数据准备
DbUnit支持使用xml文件来定义测试数据(包括初始数据和期望结果),在本例中,预期在初始化数据库的时候,插入两条数据,测试数据文件定义如下:
1
2
3
4
5
|
<!-- basic-test-data.xml -->
<dataset>
<tbl_todos id="1" title="Professional C#" description="4th version" status="0" />
<tbl_todos id="2" title="Core Java" description="Volume I" status="0" />
</dataset>
|
id其实是作为自增id存在的,但是DbUnit确实可以把id控制为需要的值,可以把id改为3,4做试验。不确定是否只在h2 database里生效。
insert测试,预期插入一条数据,期望结果如下:
1
2
3
4
5
6
|
<!-- expected-result-for-insert.xml -->
<dataset>
<tbl_todos id="1" title="Professional C#" description="4th version" status="0" />
<tbl_todos id="2" title="Core Java" description="Volume I" status="0" />
<tbl_todos id="3" title="Item Title" description="Item Description" status="0" />
</dataset>
|
update和delete的期望结果类似,不在此列出。
单元测试环境配置
Dao层的实现依赖Spring,单元测试也会以相同的方式获得待测Dao对象。需要准备一个专门用于Dao层单元测试的Spring配置文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<!-- spring-dao-test.xml -->
<beans>
<!-- 导入spring-dao -->
<import resource="classpath:spring-dao.xml" />
<!-- 覆盖原dataSource,使用h2数据库做单元测试 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.h2.Driver" />
<property name="url" value="jdbc:h2:mem:todo-list;DB_CLOSE_DELAY=-1;MODE=MySQL" />
</bean>
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
<!-- 执行下面script完成初始化 -->
<jdbc:script location="classpath:db/scheme.sql"/>
</jdbc:initialize-database>
</beans>
|
一般来说用于测试的spring配置文件应该尽可能保证和项目的spring配置一致,所以通常会import项目spring配置,然后把需要修改的bean覆盖掉。本例中把数据库从MySQL改为h2,以便其他团队成员不需要做前置配置即可执行unit test。但实际上借助DbUnit,用MySQL作为单元测试的依赖也很方便。
编写单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@RunWith(SpringJUnit4ClassRunner.class) //指定使用Spring Test
@ContextConfiguration(value="classpath:spring-dao-test.xml") //加载Spring Context
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class, //为了让Spring处理DbUnit的annotation,
DbUnitTestExecutionListener.class }) //必须使用的ExecutionListener
public class TodoDaoTest {
@Autowired
private TodoDao todoDao;
@Test
@DatabaseSetup(value = {"basic-test-data.xml"}) //指定数据库初始化数据
public void testSelectAll() throws InterruptedException {
List<TodoItem> todoItems = todoDao.selectAll();
assertEquals(2, todoItems.size());
}
@Test
@DatabaseSetup(value="basic-test-data.xml") //指定数据库初始化数据
@ExpectedDatabase(value="expected-result-for-insert.xml") //指定数据库期望结果
public void testInsert() throws InterruptedException {
TodoItem todo = new TodoItem();
todo.setTitle("Item Title");
todo.setDescription("Item Description");
todo.setStatus(0);
int impactRows = todoDao.insert(todo);
assertEquals(1, impactRows);
}
//其他测试方法类似,略。
}
|
注意
- 由于Dao依赖Spring生成,需要配置RunWith以及ContextConfiguration。如果不用,那么需要自己写代码读取配置文件,加载Spring Context。
- 使用Spring Test DbUnit,必须配置好DbUnitTestExecutionListener及其依赖DependencyInjectionTestExecutionListener。
- @DatabaseSetup在执行的时候会清空数据库,再插入初始化数据。如果期望数据库在测试方法结束回滚,需要添加下面代码:
1
|
@DatabaseTeardown(value="basic-test-data.xml", DatabaseOperation.DELETE) //删除data-set里的数据。
|
- 编写testInsert的时候,原来打算在空数据库内插入数据,预期结果是数据表里面有一条id=1的数据。但是发现JUnit在执行的时候,会优先执行SELECT测试,把basic-test-data.xml的数据加载。Teardown会把数据删除,但不是恢复数据库状态,所以在执行INSERT的时候,生成的id是3而不是1。考虑到Unit Test Case之间不允许相互依赖,在这个例子中,哪怕其他方法都做了Teardown,insert也应该做DatabaseSetup,获得一个稳定的前置状态。