对于dao层的测试, 集成测试和隔离的单元测试各有优劣.
利用一些Stub的隔离的单元测试速度比较快, 但是不能够测试dao与数据库的交互是否工作.
而集成测试的速度可能因为要建立实际的数据库连接或数据库存在于远程机器上而速度要慢一些, 但它能够真正测试dao与数据库的交互是否正常工作.
在这两种测试中,我更倾向于使用集成测试.下面是我在一个项目(使用spring, hibernate)中的一些具体测试策略(希望大家讨论一下具体在TDD中每个层(controller/service/dao)的测试都是怎么做的):
1. 每一个开发人员一个数据库实例
为了保证每个测试在测试之前是一个可知的状态, 不同的开发人员不应该共享同一个数据库实例
2. 使用内存数据库HSQLDB作为测试数据库(因为有了hibernate的使用才可以在不同的数据库之间切换)
使用内存数据库会提高测试运行的速度,
并且在测试运行完毕之后,数据不会保留下来,这使得不同进程的测试不会互相干扰,一些功能测试不会与单元测试互相干扰.
使用HSQLDB的缺点是HSQLDB不是真正的Production Database Server,所以可能不会测出一些和具体数据库相关的一些错误.
另外一个很头疼的问题是HSQLDB好像根本不支持Schema.
3. 使用spring的AbstractTransactionalDataSourceSpringContextTests
使用AbstractTransactionalDataSourceSpringContextTests主要有以下原因:
1)注入被测的dao, 而不是通过application context查找.例子如下:
Java代码
public class PersonDAOTest extends AbstractTransactionalDataSourceSpringContextTests {
private PersonDAO personDAO;
public void setPersonDAO(PersonDAO personDAO); {
this.personDAO = personDAO;
}
/*************testmethods************************/
}
public class PersonDAOTest extends AbstractTransactionalDataSourceSpringContextTests {
private PersonDAO personDAO;
public void setPersonDAO(PersonDAO personDAO); {
this.personDAO = personDAO;
}
/*************testmethods************************/
}
2)为了使不同的测试方法共享data fixture(让testSave(), testGetPerson(), testRemovePerson共享person数据对象),如
Java代码
public class PersonDAOTest extends TestCase {
private Person person;
public void setUp(); {
person = new Person();;
person.setFirstName("Sean");;
person.setLastName("Liu");;
person.setUserName("forever");;
}
public void testSave(); {
personDAO.save(person);;
assertNotNull(person.getId(););;
}
public void testGetPerson(); {
personDAO.save(person);;
Person retrievedPerson = personDAO.getPerson(person.getId(););;
assertNotNull(retrievedPerson.getId(););;
assertEquals(person, retrievedPerson);;
}
public void testRemovePerson(); {
personDAO.save(person);;
person.delete(person);;
assertNull(personDAO.getPerson(person.getId();););;
}
}
public class PersonDAOTest extends TestCase {
private Person person;
public void setUp(); {
person = new Person();;
person.setFirstName("Sean");;
person.setLastName("Liu");;
person.setUserName("forever");;
}
public void testSave(); {
personDAO.save(person);;
assertNotNull(person.getId(););;
}
public void testGetPerson(); {
personDAO.save(person);;
Person retrievedPerson = personDAO.getPerson(person.getId(););;
assertNotNull(retrievedPerson.getId(););;
assertEquals(person, retrievedPerson);;
}
public void testRemovePerson(); {
personDAO.save(person);;
person.delete(person);;
assertNull(personDAO.getPerson(person.getId();););;
}
}
其中person的username是其业务主键
请注意,即使用内存数据库HSQLDB上面的代码也是不能工作的,因为不同的测试方法是在同一个数据库进程中, 所以会有主键约束错误
(例如testSave运行完后, testPerson就会抛出由于相同的username造成的主键约束错误).
感谢spring的AbstractTransactionalDataSourceSpringContextTests, 它可以使我们在一个测试起始时开始一个transaction,
在测试完毕后回滚数据,这样上面的目的就能达到了.
Java代码
public class BaseDAOTestCase extends AbstractTransactionalDataSourceSpringContextTests{
protected static Log log = LoggerServiceImpl.getLogger();;
protected String[] getConfigLocations(); {
return new String[] {"classpath*:/WEB-INF/mPlatform*ApplicationContext.xml"};
}
protected void flushSession();{
SessionFactory sessionFactory =
(SessionFactory);applicationContext.getBean("sessionFactory");;
sessionFactory.getCurrentSession();.flush();;
}
}
public class PersonDAO extends BaseDAOTestCase {
private PersonDAO personDAO;
private Person person;
public void setPersonDAO(PersonDAO personDAO); {
this.personDAO = personDAO;
}
public void onSetUpBeforeTransaction(); {
person = new Person();;
person.setFirstName("Sean");;
person.setLastName("Liu");;
person.setUserName("forever");;
}
public void testSave(); {
personDAO.save(person);;
assertNotNull(person.getId(););;
}
public void testGetPerson(); {
personDAO.save(person);;
Person retrievedPerson = personDAO.getPerson(person.getId(););;
assertNotNull(retrievedPerson.getId(););;
assertEquals(person, retrievedPerson);;
}
public void testRemovePerson(); {
personDAO.save(person);;
person.delete(person);;
flushSession();;
assertNull(personDAO.getPerson(person.getId();););;
}
}
public class BaseDAOTestCase extends AbstractTransactionalDataSourceSpringContextTests{
protected static Log log = LoggerServiceImpl.getLogger();;
protected String[] getConfigLocations(); {
return new String[] {"classpath*:/WEB-INF/mPlatform*ApplicationContext.xml"};
}
protected void flushSession();{
SessionFactory sessionFactory =
(SessionFactory);applicationContext.getBean("sessionFactory");;
sessionFactory.getCurrentSession();.flush();;
}
}
public class PersonDAO extends BaseDAOTestCase {
private PersonDAO personDAO;
private Person person;
public void setPersonDAO(PersonDAO personDAO); {
this.personDAO = personDAO;
}
public void onSetUpBeforeTransaction(); {
person = new Person();;
person.setFirstName("Sean");;
person.setLastName("Liu");;
person.setUserName("forever");;
}
public void testSave(); {
personDAO.save(person);;
assertNotNull(person.getId(););;
}
public void testGetPerson(); {
personDAO.save(person);;
Person retrievedPerson = personDAO.getPerson(person.getId(););;
assertNotNull(retrievedPerson.getId(););;
assertEquals(person, retrievedPerson);;
}
public void testRemovePerson(); {
personDAO.save(person);;
person.delete(person);;
flushSession();;
assertNull(personDAO.getPerson(person.getId();););;
}
}
请注意在父类BaseDAOTestCase中有一个flushSession方法, 这是因为hibernate缓存机制的存在会使hibernate的一些方法抛出错误,
所以需要flushSession的方法来完成.
Rod Johnson在spring 论坛中有一句话很好的总结了如何在测试中处理hibernate缓存:
引用
Remember that you can clear the Hibernate session, removing objects already associated with it. This is often necessary before requerying in tests, and solves most (if not all) problems.
I typically use JDBC for verification. The pattern is
- do Hibernate operation
- flush Hibernate session
- issue JDBC query to verify results
That way I'm verifying what Hibernate did to the database in the same transaction.
__________________
Rod Johnson - CEO, Interface21
http://www.springframework.com - Spring From the Source
Training, Consulting, Support