数据库层的单元测试对构建企业应用来说是比较有价值的,但是由于过于复杂我们不得不放弃他。Unitils降低了数据库测试的复杂度,让数据库测试简单而又容易维护,下面的章节描述DatabaseModule andDbUnitModule 怎么对你的数据库测试提供支持。
数据库测试应该使用单元测试数据库,这样你可以完全的精细的控制你使用到的测试数据。DbUnitModule 是基于DBunit构建的,可以提供对测试数据集的支持。
让我们看一个例子,UserDao有一个简单的方法findByName,用来通过用户的first 和last name来取回用户,常用的单元测试如下:
@DataSet
public class UserDAOTest extends UnitilsJUnit4 {
@Test
public void testFindByName() {
User result = userDao.findByName("doe", "john");
assertPropertyLenientEquals("userName", "jdoe", result);
}
@Test
public void testFindByMinimalAge() {
List<User> result = userDao.findByMinimalAge(18);
assertPropertyLenientEquals("firstName", Arrays.asList("jack"), result);
}
}
@DataSet注解是通知Unitils查找测试需要加载的Dbunit数据文件。如果没有指定文件名,Unitils会在当前文件夹自动查找和测试类文件名相同的数据集文件如:className.xml.
数据集文件应该使用Dbunit的 FlatXMLDataSet 格式,并且应该包含所有测测试数据。所有表的内容首先被清空,然后所有的测试数据被插入。不在数据文件中的表,是不会被清空内容的。你如果需要清空特定的表你可以在文件中加入一个表的空标签,如:<MY_TABLE />,对于插入null值,你也可以使用类似的方法。
对UserDaoTest你需要建立一个数据集文件名称为:UserDaoTest.xml 并且把它放到UserDaoTest 类文件所在的目录。
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<usergroup name="admin" />
<user userName="jdoe" name="doe" firstname="john" userGroup="admin" />
<usergroup name="sales" />
<user userName="smith" name="smith" userGroup="sales" />
</dataset>
这会清空user表和usergroup表,并且插入新的记录,用户名为smith的用户的first name会被设置null值。
支持testFindByMinimalAge()方法需要特殊的数据集而不是类级别的数据集。那么你需要建立一个文件:UserDAOTest.testFindByMinimalAge.xml 并且放在和测试类相同的文件夹即可
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user userName="jack" age="18" />
<user userName="jim" age="17" />
</dataset>
你可以用这个数据集文件通过给这个方法上标注@DataSet注解,来覆盖类的数据集文件。
public class UserDAOTest extends UnitilsJUnit4 {
@Test
@DataSet("UserDAOTest.testFindByMinimalAge.xml")
public void testFindByMinimalAge() {
List<User> result = userDao.findByMinimalAge(18);
assertPropertyLenientEquals("firstName", Arrays.asList("jack"), result);
}
}
方法级别的数据集文件不应该被过度使用,因为过多的数据文件意味这更多的维护工作,你应该尽量减少数据在类级别的数据集,多数情况下比较小的数据集就可以给多个单元测试公用。但是如果公用数据导致了数据量的增大和杂乱,那么用方法级别的数据集,或者细分单元测试类,每个类用自己的数据集。
给一个类或者其父类通过@DataSet设置的数据集对类里的每个测试方法都有效。如果一个数据集只被几个测试方法使用,那么你最好不要把他们放在类级别的注解里,而应该在相应的测试方法上加上注解。如果你的数据集文件没有像我们前面描述的那样命名,你也可以自己命名,当然只需要在@dataset注解中标明即可,你也可以使用多个数据集文件,如下示例:
@DataSet({"UserDAOTest_general.xml", "ConfigSettings.xml"})
public class UserDAOTest extends UnitilsJUnit4 {
@Test
public void testFindByName() {
User result = userDao.findByName("doe", "john");
assertPropertyLenientEquals("userName", "jdoe", result);
}
@Test
@DataSet("UserDAOTest_ages.xml")
public void testFindByMinimalAge() {
List<User> result = userDao.findByMinimalAge(18);
assertPropertyLenientEquals("firstName", Arrays.asList("jack"), result);
}
}
默认情况下,数据集加载是,采用先清除后插入的策略。那就意味着所有涉及的表中数据会被删除,然后再插入测试数据。这个动作时可以被设置的,你可以通过如下配置来编辑DbUnitModule.DataSet.loadStrategy.default,
如果我们在Unitils.properties文件中这么修改:
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.InsertLoadStrategy
这样设置的话,就不会先删除现有数据了,而只是插入数据。
加载策略也可以对特定的测试类来设置,需要在@DataSet注解中这么标注:
@DataSet(loadStrategy = InsertLoadStrategy.class)
如果你熟悉Dbunit,配置加载策略类似于使用不同的数据库,下面是默认支持的加载策略:
Unitils的数据集文件使用multischema xml 格式,这是DbUnits FlatXmlDataSet 格式的一个扩展版本。数据集工厂来管理文件格式的配置和文件扩展。
尽管Unitils目前只支持一种数据集格式,但是通过自定义的数据集工厂的实现是可以支持不同的文件格式的。
你可以通过Unitils.propertis文件的DbUnitModule.DataSet.factory.default 属性配置或者在@DataSet注解中标明。例如你可以建立一个DbUnit的XlsDataSet通过实现DataSetFactory来使用Excel文件作为数据集文件。
在测试运行后,有时候使用数据集的数据来对比数据库的内容是比较有用的。例如当你想检查大量数据更新或者存储过程执行的结果。
下面的例子测试一个方法,这个方法禁用所有一整年没有活动的用户的帐号:
public class UserDAOTest extends UnitilsJUnit4 {
@Test @ExpectedDataSet
public void testInactivateOldAccounts() {
userDao.inactivateOldAccounts();
}
}
注意我们在这个方法上增加了@ExpectedDataSet 注解。这会让Unitils寻找数据集文件UserDAOTest.testInactivateOldAccounts-result.xml并且比较数据库内容和数据集的数据。
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user userName="jack" active="true" />
<user userName="jim" active="false" />
</dataset>
对这个数据集,Unitils会检查是否有两个不同的用户记录在用户表。其他的记录或其他表不会涉及。
使用@DataSet注解,文件名是可以被指定的,如果名字没有指定,那么会查找类似格式文件名的文件: className.methodName-result.xml
结果数据集应该尽量小。数据多意味着维护量变大。还有,最好尽量执行相同的检查在测试代码里。
一些应用使用了多个数据库。为了实现这个功能,Unitils的数据集XML文件中可以对多数据库进行支持。下面的例子展现了怎么为两个数据库的数据表装载数据。
<?xml version='1.0' encoding='UTF-8'?>
<dataset xmlns="SCHEMA_A" xmlns:b="SCHEMA_B">
<user id="1" userName="jack" />
<b:role id="1" roleName="admin" />
</dataset>
这个例子我们定义了两个方案,A和B,schema-A和默认的XML命名空间关联,schema B关联命名空间b.如果一个表的描述中有前缀指明哪个命名空间,那么就会用指定的,否则默认schema A。
如果没有默认的命名空间,那么系统会默认在database.schemaNames属性中第一个schema。所以系统是支持你如下定义的。
database.schemaNames=SCHEMA_A, SCHEMA_B
这样系统默认schema-A 为默认的schema,那么你就可以不用再声明默认的schema了。
<?xml version='1.0' encoding='UTF-8'?>
<dataset xmlns:b="SCHEMA_B">
<user id="1" userName="jack" />
<b:role id="1" roleName="admin" />
</dataset>
上面的例子我们没有提到一件重要的事情,连接测试数据库的数据源在哪里,并且怎么让我们的UseDao使用这个数据源?
当你的第一个数据库测试在测试组件中运行的时候,Unitils会使用配置的属性建立一个数据源实例,去连接测试数据库。后面的数据库测试会仍会使用这个数据源,连接的详细属性描述如下:
database.driverClassName=oracle.jdbc.driver.OracleDriver
database.url=jdbc:oracle:thin:@yourmachine:1521:YOUR_DB
database.userName=john
database.password=secret
database.schemaNames=test_john
通常驱动和url配置放在Unitils.properties文件里作为项目共用,用户密码可以放在Unitils-local.properties作为每个开发者自己的设置。这样可以让每个开发者使用自己的测试数据,防止互相干扰。
当一个测试开始执行的时候,数据源示例会被注入到测试实例中,如果一个属性或者setter方法用注解@TestDataSource指定了,那么系统会使用你指定的数据源。你仍需要提供一些项目特定的代码来让你的代码使用这个数据源。通常这些配置在项目父类中被执行一次,简单的示例如下:
public abstract class BaseDAOTest extends UnitilsJUnit4 {
@TestDataSource
private DataSource dataSource;
@Before
public void initializeDao() {
BaseDAO dao = getDaoUnderTest();
dao.setDataSource(dataSource);
}
protected abstract BaseDAO getDaoUnderTest();
}
The above example uses annotations to get a reference to the datasource. Another way of making your code use the Unitils DataSource is by callingDatabaseUnitils.getDataSource().
上面的例子使用注解获取一个指定的数据源。另一个使用UnitilsDataSource的方法是使用DatabaseUnitils.getDataSource().
很多情况下我们我们要以事务的方式存取数据,例如:
By default every test is executed in a transaction, which is committed at the end of the test.
默认状况下每个以事务方式执行的测试,完成后数据会被提交。
默认的动作可以通过配置来修改,如:
DatabaseModule.Transactional.value.default=disabled
可选的值还有: commit, rollback and disabled.
事务行为可以在测试类中被修改,会用到 @Transactional.注解如:
@Transactional(TransactionMode.ROLLBACK)
public class UserDaoTest extends UnitilsJUnit4 {
这样这个测试类的每个测试执行后数据将回滚。@Transactional 注解是可以被继承的。如果有必要你可以在你测试类的父类中使用。
其实,Unitils依赖Spring的事务管理,但这不意味着你必须使用Spring在你的应用代码里。实际上对用户来说那是透明的。
如果你继承了Unitils和Spring,并且你已经配置了bean的类型PlatformTransactionManager 在Spring配置里。Unitils会使用这个事务管理器。