数据库测试
在创建企业级应用的时候,数据层的单元测试因为其复杂性往往被遗弃,Unitils大大降低了测试的复杂性,使得数据库的测试变得容易并且易维护。已下介绍databasemodule和dbunitmodule进行数据库的单元测试。
用dbUnit管理测试数据
数据库的测试应该在单元测试数据库上运行,单元测试数据库给我们提供了一个完整的并有着很好细粒度控制的测试数据,DbUnitModule是在dbunit的基础上进一步的为数据库的测试提供数据集的支持。
加载测试数据集
让我们以UserDAO中一个简单的方法findByName(检查姓氏和名字)为例子开始介绍。他的单元测试如下:
@DataSet
public class UserDAOTest extends UnitilsJUnit4 {
@Test
public void testFindByName() {
User result = userDao.findByName("doe", "john");
assertPropertyLenEquals("userName", "jdoe", result);
}
@Test
public void testFindByMinimalAge() {
List<User> result = userDao.findByMinimalAge(18);
assertPropertyLenEquals("firstName", Arrays.asList("jack"), result);
}
}
@DateSet 注解表示了测试需要寻找dbunit的数据集文件进行加载,如果没有指明数据集的文件名,则Unitils自动在class文件的同目录下加载文件名为 className.xml的数据集文件。(这种定义到class上面的数据集称为class级别的数据集)
数据集 文件必须是dbunit的FlatXMLDataSet文件格式,其中包含了所要测试的数据。测试数据库表中所有的内容将会被删除,然后再插入数据集中的 数据。如果表不属于数据集中的,哪么该表的数据将不会被删除。你也可以明确的加入一个空的表元素,例如<MY_TABLE/>(可以达到删除 测试数据库表中内容的作用),如果要明确指定一个空的值,那么使用值[null]。
为UserDAOTest我们创建一个数据集,并放在UserDAOTest.class文件同目录下。
<?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>
测试运行的时候,首先将删除掉usergroup表和user表中的所有内容,然后将插入数据集中的内容。其中name为smith的firstname的值将会是null。
假设testFindByMinimalAge()方法将使用一个特殊的数据集而不是使用class级别的数据集,你可以定义一个UserDAOTest.testFindByMinimalAge.xml 数据集文件并放在测试类的class文件同目录下。
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
<user userName="jack" age="18" />
<user userName="jim" age="17" />
</dataset>
这时,你在testFindByMinimalAge()方法使用@DataSet注解,他将覆盖class级的数据集
public class UserDAOTest extends UnitilsJUnit4 {
@Test
@DataSet("UserDAOTest.testFindByMinimalAge.xml")
public void testFindByMinimalAge() {
List<User> result = userDao.findByMinimalAge(18);
assertPropertyLenEquals("firstName", Arrays.asList("jack"), result);
}
}
不要过多的使用method级的数据集,因为过多的数据集文件意味着你要花大量的时间去维护,你优先考虑的是使用class级的数据集。
配置数据集加载策略
缺省情况下数据集被写入数据库采用的是clean insert策略。这就意味着数据在被写入数据库的时候是会先删除数据集中有使用的表的数据,然后在将数据集中的数据写入数据库。加载策略是可配额制的,我们通过修改DbUnitModule.DataSet.loadStrategy.default 的属性值来改变加载策略。假设我们在unitils.properties属性文件中加入以下内容:
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.InsertLoadStrategy
这时加载策略就由clean insert变成了insert,数据已经存在表中将不会被删除,测试数据只是进行插入操作。
加载策略也可以使用@DataSet的注解属性对单独的一些测试进行配置:
@DataSet(loadStrategy = InsertLoadStrategy.class)
对于那些树形DbUnit的人来说,配置加载策略实际上就是使用不同的DatabaseOperation,以下是默认支持的加载策略方式:
l CleanInsertLoadStrategy: 先删除dateSet中有关表的数据,然后再插入数据。
l InsertLoadStrategy: 只插入数据。
l RefreshLoadStrategy: 有同样key的数据更新,没有的插入。
l UpdateLoadStrategy: 有同样key的数据更新,没有的不做任何操作。
配置数据集工厂
在Unitils中数据集文件采用了multischema xml 格式,这是DbUnits的FlatXmlDataSet 格式的扩展。配置文件格式和文件的扩展可以采用DataSetFactory 。
虽然Unitils当前只支持一种数据格式,但是我们可以通过实现DataSetFactory来使用其他文件格式。当你想使用excel而不是xml格式的时候,可以通过unitils.property中的DbUnitModule.DataSet.factory.default 属性和@DataSet 注解来创建一个DbUnit's XlsDataSet 实例。
验证测试结果
有些时候我们想在测试时完毕后使用数据集来检查数据库中的内容,举个例子当执行完毕一个存储过程后你想检查一下啊数据是否更新了没有。
下面的例子表示的是禁用到一年内没有使用过的帐户
public class UserDAOTest extends UnitilsJUnit4 {
@Test @ExpectedDataSet
public void testInactivateOldAccounts() {
userDao.inactivateOldAccounts();
}
}
注意在test方法上增加了一个@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>
根据这个数据集,将会检查是否有两条和记录集的值相同的记录在数据库中。而其他的记录和表将不理会。
使用的是@DataSet 注解的话,文件名可以明确指出,如果文件名没有明确指出来,那么文件名将匹配className .methodName -result.xml
使用少使用结果数据集,加入新的数据集意味着更多的维护。替代方式是在代码中执行相同的检查(如使用一个findactiveusers()方法)。
使用多模式的数据集
一个程序不单单只是连接一个数据库shema。Unitils采用了扩展的数据集xml来定义多schemas下的数据。以下就是一个读取数据到2个不同的schemas中的例子:
<?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>
在这个例子中我定义了两个schemas,SCHEMA_A 和 SCHEMA_B。第一个schema,SCHEMA_A 被连接到默认的xml命名空间中,第二个schema,SCHEMA_B 被连接到命名空间b。如果表xml元素的前缀使用了命名空间b,那么该表就是schema SCHEMA_B 中的,如果没有使用任何的命名空间那么该表将被认为是SCHEMA_A
中的。以上例子中测试数据定义了表SCHEMA_A.user 和SCHEMA_B.role。
如果在数据集中没有配置一个默认的命名空间,那么将会采用在unitils.properties中的属性database.schemaNames 的第一个值作为默认的
database.schemaNames=SCHEMA_A, SCHEMA_B
这个配置将SCHEMA_A 作为缺省的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>
连接测试数据库
在以上所有的例子中,我们都有一件重要的事情没有做:当我们进行测试的时候,怎样连接数据库并得到DataSource?
当测试套件的第一个测试数据库的案例运行的时候,Unitils将会通过属性文件创建一个DataSource 的实例来连接你单元测试时的数据库,以后的测试中都将使用这个DataSource 实例。连接配置的详细内容如下:
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 中去,而用户名,密码以及schema可以配置到unitils-local.properties 中去,这样可以让开发人员连接到自己的单元测试数据库中进行测试而不会干预到其他的人。
在属性或者setter方法前使用注解@TestDataSource ,将会将DataSource 实例注入到测试实例中去,如果你想加入一些代码或者配置一下你的datasource,你可以做一个抽象类来实现该功能,所有的测试类都继承该类。一个简单的例子如下:
public abstract class BaseDAOTest extends UnitilsJUnit4 {
@TestDataSource
private DataSource dataSource;
@Before
public void initializeDao() {
BaseDAO dao = getDaoUnderTest();
dao.setDataSource(dataSource);
}
protected abstract BaseDAO getDaoUnderTest();
}
上面的例子采用了注解来取得一个datasource的引用,另外一种方式就是使用DatabaseUnitils.getDataSource() 方法来取得datasource。
事务
出于不同的原因,我们的测试都是运行在一个事务中的,其中最重要的原因如下:
l 数据库的很多action都是在事务正常提交后才做,如SELECT FOR UPDATE 和触发器
l 许多项目在测试数据的时候都会填写一些测试数据,每个测试运行都会修改或者更新了数据,当下一个测试运行的时候,都需要将数据回复到原有的状态。
l 如果使用的是hibernate或者JPA的时候,都需要每个测试都运行在事务中,保证系统的正常工作。
缺省情况下,事务管理是disabled的,事务的默认行为我们可以通过属性文件的配置加以改变:
DatabaseModule.Transactional.value.default=commit
采用这个设置,每个的测试都将执行commit,其他的属性值还有rollback 和disabled
我们也可以通过在测试类上使用注解@Transactional 来改变默认的事务设置,如:
@Transactional(TransactionMode.ROLLBACK)
public class UserDaoTest extends UnitilsJUnit4 {
通过这种class上注解的事务管理,可以让每个测试都确保回滚,@Transactional 注解还可以继承的,因此我们可以将其放在父类中,而不必每个子类都进行声明。
.........
如果你使用Unitils的spring支持(见使用spring进行测试)你如果配置了PlatformTransactionManager 的bean,那么unitils将会使用这个事务管理。