业务层和持久层单元测试的实践单元测试大多数时候遇到的问题难点,其实就在于怎么样很好的解决各种依赖(我
业务层和持久层单元测试的实践
单元测试大多数时候遇到的问题难点,其实就在于怎么样很好的解决各种依赖(我这里分为外部依赖和持久层依赖吧)。根据笔者的实践,总结了如下一些实践经验,希望能够和大家共同提高。如无特殊说明:以下例子采用junit4 + jmock1.0。
1.如何解决外部系统的依赖?
对于业务层的单元测试,比较复杂的情况就是会有很多外部services(接口或者服务等)的依赖,其实对于我们来说最重我们需要得到或者传递出去的都是各种数据对象,那么这种情况下可以采用mock来模拟这些数据对象。这样让测试方便的通过。
场景:测试DefaultGetNumAO类的getNum方法,在真正的应用里面,调用此方法的前提是从session中取得用户信息,也就是登陆信息,但是单元测试的时候是拿不到的,如果我们直接用代码测试,那么还没有执行到getNum,单元测试就会报错,提示没有登录,其实是说这个用户信息没有取到。在线上环境我们是不会存在这个问题的。我们可以用以下代码模拟登录的用户数据(SysUser,实现的是IUser接口),这个时候我们可以用mock的方式来模拟这个外部依赖接口(或者服务)。
采用jmock的话,有两种,一种是采用接口模拟(org.jmock.Mock)的方式,一种没有实现接口的类的MOCK,是基于cglib(字节码)的方式。在这里我用的是接口模拟,我先模拟出IUser接口对象:
// 构造Mock控制器 Mock m = new Mock(IUser.class); // 这是要测试MockObject IUser mock = (IUser) m.proxy(); // 期待的返回值 SampleReturn sr = new SampleReturnImpl(); // 期待的参数 Parameter p = new ParameterImpl(); // 控制器,期待一次,方法sampleMethod,参数等于p(equals),将返回sr m.expects(once()).method("getUserId").with(eq(p)).will(returnValue(sr));
其实从上面可以看出来,JMOCK或者其他Mock也好,其思想就是模拟对象,建立孤立的测试环境,上述DefaultGetNumAO类调用过多的外部系统,我们将外部系统的调用入口统一MOCK掉,MOCK出我们自己想要的数据;
我们采用下面的代码:
DefaultGetNumAO dftAo = new DefaultGetNumAO();//这里手工输入你的mock出来的对象(外部依赖)dftAo.setDftAo(dftAo);//执行真正的要测试函数Result result = dftAo. getNum ();//过程中会调用你注入的对象替代真正运行的对象或者服务Assert.assertTrue(result.isSuccess());
采用上述的方式,可以将DefaultGetNumAO里面所有依赖的外部调用全部mock掉。
2.如何解决对数据库的依赖?对于DAO层来说,直接修改数据库中的物理数据,可能会带来众多冗余数据或者引起数据紊乱等情况。
场景:比如你需要测试一个insert语句,用传统的junit测试的方式,当你插入成功之后,这些你插入的测试数据实际上就成了冗余数据;更可能存在的情况,你插入这条数据之后,有些有唯一性约束的字段存在的时候,就不能再次执行插入了,这就意味着你的单元测试代码只能执行一次。
一般这种情况下采用如下方式解决:
第一,在测试代码开始前插入临时数据,然后执行你的测试代码,待正真要测试的代码执行结束之后,在你的单元测试代码之后清理掉插入的临时数据。
如下例子:
//设置全局变量,保存数据用private long iTestAllId = 0L;/** * * setUpBeforeClass * 初始化操作 * @throws DAOException * @since 1.0.0 */@BeforeClasspublic void setUpBeforeClass() throws Exception {//先执行插入操作,得到临时数据iTestAllIdtestInsertCheckList();//进行赋值if(iTestAllId > 0) {checkListDO.setId(iTestAllId);}} //执行结束之后,将数据清理:/** * 数据清理操作 */@AfterClasspublic void tearDownAfterClass() throws Exception {//删除数据testDeleteCheckList();}
第二,在上面的基础上,手工控制事物,在测试代码执行完成之后,事物回滚,数据库恢复到刚开始执行的地方;
第三,采用DBUnit(DBUnit的设计理念就是在测试之前,备份数据库,然后给对象数据库植入我们需要的准备数据,最后,在测试完毕后,读入备份数据库,回溯到测试前的状态,这个工具大家可以详细的在网上去查看)来模式整个数据库表,所有的操作(增删改查)都不会影响我们真实的数据库数据。
3.单元测试进阶写代码有个原则:一般不建议在代码中写hard code.单元测试也是这样。
如果我们在代码里面写大量的hard code,既不美观,修改起来也很不方面。
那么测试数据怎么引入呢?
场景:
我们要测试CheckListDAO.java这个DAO,里面有insertCheckList方法,其参数是CheckListDO这样一个普通的对象bean.
传统的写法:
先NEW出对象CheckListDO checkDo=new CheckListDO ();然后赋值//checkDo.setId(2L);checkDo.setType(13L);checkDo.setName("liqf");checkDo.setDesc("test");
然后在单元测试代码里面传入insertCheckList方法
类似这样:
int iRtn = CheckListDAO. insertCheckList (checkDo); Assert.assertNotNull(iRtn > 0);
我们如何做?
利用spring的IOC,我们将CheckListDO交给spring去管理,所有的测试数据写在XML配置文件里面,比如可以建立一个data目录,下面建立test-dao-data.xml配置文件。
如下:
<bean id="checkListDO" singleton="true"> <property name="id" value="100462" /> <property name="type" value="2" /> <property name="name" value="11" /> <property name="desc" value="11111111" /> </bean>
在测试的基类里面:
public void setUp() throws Exception {try { appContext = new ClassPathXmlApplicationContext(new String[] { "data/test-dao-data.xml" });}
这样即使我们想修改单元测试案例,只需要修改test-dao-data.xml文件中配置的数据就可以了,是不是方便很多?
采用上述的方式,我们可以将单元测试的代码和数据相分离,这样会减少很多测试代码;
同时修改测试数据也会很方便;4.Spring-mock简介Spring mock直接提供了事物的自动回滚,这点是非常方便的,所以我们拿它来做DAO层的测试的时候,一点也不用关心持久层的事物处理。避免了脏数据。简化了代码
SPRING-MCOK方式,你只需要采用如下三个步骤就可以很容易实现了:
?4.1首先继承AbstractTransactionalSpringContextTests(需要spring-mock.jar)
?4.2重载getConfigLocations() {}方法 –这个方法里面你需要手工载入所有的配置文件,包括bean的,sql的配置
?4.3写测试函数
个人以为:采用spring mock的方式将会更加容易简单的测试DAO层的数据。
1 楼 smallbee 2011-01-20 DBUnit在多开发人员同数据库情况下,是不是有问题啊? 2 楼 scholers 2011-01-20 smallbee 写道DBUnit在多开发人员同数据库情况下,是不是有问题啊?
你说的这个是一个问题,多线程的情况下,还没有研究过,不知道它有没有这种机制避免这种问题;
不过它在数据库很庞大的系统做的时候,临时存储这么多数据可能会引起很大的性能问题啊。 3 楼 finallygo 2011-01-20 楼主的文章让我学到不少,但是为什么说"代码中写hard code."不好呢?
首先用spring我觉得主要亮点就是可以使代码变得简洁,同时可以不用修改代码实现功能的替换(也就是降低了耦合),但是做测试的时候并没有这两个需求啊
如果用了spring的话,我觉得还更麻烦,尤其是测试数据多的时候,要查找也费力吧,而直接写代码里修改不是更方便吗?你说"单元测试的代码和数据相分离,这样会减少很多测试代码"指的是java的代码吧,总的工作量应该是一样的 4 楼 scholers 2011-01-20 finallygo 写道楼主的文章让我学到不少,但是为什么说"代码中写hard code."不好呢?
首先用spring我觉得主要亮点就是可以使代码变得简洁,同时可以不用修改代码实现功能的替换(也就是降低了耦合),但是做测试的时候并没有这两个需求啊
如果用了spring的话,我觉得还更麻烦,尤其是测试数据多的时候,要查找也费力吧,而直接写代码里修改不是更方便吗?你说"单元测试的代码和数据相分离,这样会减少很多测试代码"指的是java的代码吧,总的工作量应该是一样的
这位兄弟说的不错,其实大部分公司或者程序员对单元测试的代码要求不高;测试数据和测试代码分离的想法来源于,测试数据的不稳定性,比如说要测试一个根据ID查询用户的函数,可能你的这个ID经常变换,总是修改代码不是很方便,可以直接来修改XML数据文件;
引申一下,如果能够做到自动化的单元测试,直接可以输入测试数据来推动测试,那就更加好了。
如果只为单元测试,确实不用理会分离这个想法,凡事有好处也有坏处,象SPRING,一堆的配置文件,查找起来也麻烦的。
5 楼 shaka 2011-03-09 单元测试自动化,主要是数据处理这部分比较麻烦,不知道楼主对RoR框架的单元测试是否了解,我觉得那种测试解决方案是比较舒服的,可是Java里好像没有类似的解决方案 6 楼 scholers 2011-03-09 shaka 写道单元测试自动化,主要是数据处理这部分比较麻烦,不知道楼主对RoR框架的单元测试是否了解,我觉得那种测试解决方案是比较舒服的,可是Java里好像没有类似的解决方案
是的,数据的处理会比较麻烦,RoR这种方式我不太了解呢。