论坛首页 软件开发和项目管理版 DAO

DAO测试策略(spring, hibernate)

浏览 4788 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
时间:2006-02-28
对于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查找.例子如下:
[code:1]
public class PersonDAOTest extends AbstractTransactionalDataSourceSpringContextTests {
private PersonDAO personDAO;

public void setPersonDAO(PersonDAO personDAO) {
this.personDAO = personDAO;
}

/*************testmethods************************/
}
[/code:1]
2)为了使不同的测试方法共享data fixture(让testSave(), testGetPerson(), testRemovePerson共享person数据对象),如
[code:1]
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()));
}
}[/code:1]
其中person的username是其业务主键

请注意,即使用内存数据库HSQLDB上面的代码也是不能工作的,因为不同的测试方法是在同一个数据库进程中, 所以会有主键约束错误
(例如testSave运行完后, testPerson就会抛出由于相同的username造成的主键约束错误).
感谢spring的AbstractTransactionalDataSourceSpringContextTests, 它可以使我们在一个测试起始时开始一个transaction,
在测试完毕后回滚数据,这样上面的目的就能达到了.
[code:1]
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()));
}
}
[/code:1]
请注意在父类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
   
时间:2006-03-01
只需要在删除数据的地方使用flushSession()吗?
   
0 请登录后投票
时间:2006-03-01
不一定, 看具体情况, 但一般和缓存相关的问题, 都可以这样来解决.
   
0 请登录后投票
时间:2006-03-01
2. 使用内存数据库HSQLDB作为测试数据库(因为有了hibernate的使用才可以在不同的数据库之间切换)
我在sqlserver上测,换到DB2上就一堆毛病了。。。别提HSQLDB了。。。我恨DB2....
   
0 请登录后投票
时间:2006-03-01
全部重写spring的mock下关于数据库的测试类,将顶层的AbstractDependencyInjectionSpringContextTests的field applicationContext置为static,

再重写一些方法增加判断如果applicationContext不为null则不用reload spring配置文件

这样测试每一个方法就不会重新reload spring的配置文件

再加上所有的DAO放在Test Suite中,测试一百多个dao方法也就3,4十秒(与单个dao测试差不多)

我现在的做法是这样,其它层的测试也使用static的applicationContext
不过现在新写的service层测试开始使用mock测试,以前也是集成测试的
   
0 请登录后投票
时间:2006-03-02
badqiu写道:
引用

全部重写spring的mock下关于数据库的测试类,将顶层的AbstractDependencyInjectionSpringContextTests的field applicationContext置为static,

再重写一些方法增加判断如果applicationContext不为null则不用reload spring配置文件

这样测试每一个方法就不会重新reload spring的配置文件

再加上所有的DAO放在Test Suite中,测试一百多个dao方法也就3,4十秒(与单个dao测试差不多)

我现在的做法是这样,其它层的测试也使用static的applicationContext


我觉得你没有必要这么做, 因为AbstractDependencyInjectionSpringContextTests已经cache 了application context.

AbstractDependencyInjectionSpringContextTests继承了AbstractSpringContextTests, 而AbstractSpringContextTests实现了cache.下面是关于AbstractSpringContextTests的javadoc:
引用

Maintains a static cache of contexts by key. This has significant performance benefit if initializing the context would take time.
   
0 请登录后投票
时间:2006-03-02
sean 写道
badqiu写道:
引用

全部重写spring的mock下关于数据库的测试类,将顶层的AbstractDependencyInjectionSpringContextTests的field applicationContext置为static,
... 略


我觉得你没有必要这么做, 因为AbstractDependencyInjectionSpringContextTests已经cache 了application context.

AbstractDependencyInjectionSpringContextTests继承了AbstractSpringContextTests, 而AbstractSpringContextTests实现了cache.下面是关于AbstractSpringContextTests的javadoc:
引用

Maintains a static cache of contexts by key. This has significant performance benefit if initializing the context would take time.


沒錯,用AbstractDependencyInjectionSpringContextTests測試就可以發現它有cache住application context,因為只有啟動第一個AbstractDependencyInjectionSpringContextTests時速度較慢,其他只要是AbstractDependencyInjectionSpringContextTests的subclass,速度都很快,而且所有的AbstractTransactionalSpringContextTests的subclass都有rollback功能,不會汙染到database,所以我們常常在整合之前,會對真正在運行的database run一下相關的test case,不過,開發時,還是以每個人自已本機上的database為主
   
0 请登录后投票
时间:2007-05-21
我觉得这个方法比较好。

对DAO还有什么高招?如果项目中没有使用Spring的话,用什么呢?

sean 写道
对于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查找.例子如下:
[code:1]
public class PersonDAOTest extends AbstractTransactionalDataSourceSpringContextTests {
private PersonDAO personDAO;

public void setPersonDAO(PersonDAO personDAO) {
this.personDAO = personDAO;
}

/*************testmethods************************/
}
[/code:1]
2)为了使不同的测试方法共享data fixture(让testSave(), testGetPerson(), testRemovePerson共享person数据对象),如
[code:1]
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()));
}
}[/code:1]
其中person的username是其业务主键

请注意,即使用内存数据库HSQLDB上面的代码也是不能工作的,因为不同的测试方法是在同一个数据库进程中, 所以会有主键约束错误
(例如testSave运行完后, testPerson就会抛出由于相同的username造成的主键约束错误).
感谢spring的AbstractTransactionalDataSourceSpringContextTests, 它可以使我们在一个测试起始时开始一个transaction,
在测试完毕后回滚数据,这样上面的目的就能达到了.
[code:1]
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()));
}
}
[/code:1]
请注意在父类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
   
0 请登录后投票
时间:2007-05-21
看大家说了这么多关于spring的测试基类,大家有没有碰到过使用AbstractTransactionalSpringContextTests测试正确,但是换成AbstractDependencyInjectionSpringContextTests却会失败的情况,粗略一考虑,可能由于第一个实际并未将数据插入数据库,所以有些底层的异常没有产生,而第二个则将数据插入了数据库,才算是真正的进行了数据库的交互的测试。所以在测试的时候总是不敢使用第一个基类。怕一旦项目部署之后却发现单元测试没发现的问题
   
0 请登录后投票
时间:2007-05-22
jncz 写道
看大家说了这么多关于spring的测试基类,大家有没有碰到过使用AbstractTransactionalSpringContextTests测试正确,但是换成AbstractDependencyInjectionSpringContextTests却会失败的情况,粗略一考虑,可能由于第一个实际并未将数据插入数据库,所以有些底层的异常没有产生,而第二个则将数据插入了数据库,才算是真正的进行了数据库的交互的测试。所以在测试的时候总是不敢使用第一个基类。怕一旦项目部署之后却发现单元测试没发现的问题
不是因为事务的问题,是因为没有hibernate session 没有同步数据库的问题。
   
0 请登录后投票
论坛首页 软件开发和项目管理版 DAO

跳转论坛:
JavaEye推荐