断断续续地学习了一些单元测试的知识,在最近的编码过程中有意识地进行了实践,勉强能达到一点测试的既定目的,但感觉疑惑仍然不少。
在javaeye上也拜读了诸多高人们关于单元测试、TDD方面的文章,获益良多,但是感觉很多文章起点有些高,像我这样比较笨的人读多次都不一定能领悟,适合入门一级的测试文章不太多。因此我想将自己实施单元测试的一些实践整理出来,尽量表述出我的想法,尽量提供比较详细的代码,希望初次接触单元测试的朋友能从中受益,从而少走一些弯路。另外,我在学习和实施单元测试的过程中也有很多不解和困惑,希望可以得到大家的指点。
先列出一个测试代码实例吧。
业务逻辑:对员工信息的增删改查;
业务对象:EmpBO;
业务对象接口代码(部分):
java 代码
-
-
-
-
-
-
-
- public interface EmpBOI {
-
-
-
-
-
-
-
-
- public ArrayList saveNewEmp(CcpAclsUserExtForm fmCau);
-
-
-
-
-
-
-
-
-
- public ArrayList saveEmp(CcpAclsUserExtForm fmCau);
-
-
-
-
-
-
-
-
-
- public ArrayList removeEmp(String ccpUserGuid);
-
-
-
-
-
-
-
-
- public ArrayList getEmpById(String ccpUserGuid);
-
- }
这段代码反映了典型的CRUD业务逻辑操作,对实现类的测试基本说明了我在实践单元测试时遇到的问题。
对于写代码的顺序,我一般是先根据概设文档定义出业务逻辑操作类接口,测试类,业务逻辑类,然后针对每一个接口方法,先写该方法的测试用例,然后在业务逻辑类中实现该方法,运行测试,修改或重构实现代码。
下面来看测试代码。
首先定义了一个测试基类,扩展自Spring提供的JUnit封装类AbstractDependencyInjectionSpringContextTests。
测试基类代码:
java 代码
- public abstract class SpringUnitTest extends AbstractDependencyInjectionSpringContextTests{
-
-
-
-
- protected String[] getConfigLocations() {
-
- setAutowireMode(AUTOWIRE_BY_NAME);
- return new String[]{Constants.DEFAULT_SPRING_CONTEXT_HIB,
- Constants.DEFAULT_SPRING_CONTEXT_SER,
- Constants.DEFAULT_SPRING_CONTEXT_RES,
- Constants.DEFAULT_TEST_SPRING_CONTEXT_CCP};
- }
- }
SpringUnitTest类扩展AbstractDependencyInjectionSpringContextTests,实现了其抽象方法getConfigLocations,将Spring对bean的匹配方式设为AUTOWIRE_BY_NAME,根据名称而非type查找bean,然后返回Spring相关 配置文件的路径。配置文件中定义了DataSource数据源,测试代码运行时连接数据库。
对员工管理BO的测试类EmpBOTest定义:
java 代码
- public class EmpBOTest extends SpringUnitTest {
- protected EmpBOI empBO;
-
- public void setEmpBO(EmpBOI empBO) {
- this.empBO = empBO;
- }
- }
测试类中注入了待测试的业务类。
首先,写新增员工方法saveNewEmp的测试代码:
java 代码
- public void testSaveNewEmp(){
- CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm);
- fmCau.setCauGuid("9DDC036A177088F0FAE833CEA0971DF0");
- fmCau.setName("吕南");
- fmCau.setSex("02");
- fmCau.setBirth("1989-11-24");
- fmCau.setAddress("北京市大兴区");
- fmCau.setMobile("13810384254");
- fmCau.setWorkUnit("威天软件");
- fmCau.setTel("01055669235");
- ArrayList retlist = this.ccpUserBO.saveNewEmp(fmCau);
- assertEquals("true",(String)retlist.get(0));
- }
然后在业务实现类EmpBO中具体实现该方法的业务逻辑。
说明一下,CcpAclsUserExtForm是对员工pojo对象的封装,员工对象的主键是cauGuid,这是一个由程序依据一定规则随机产生的32位字符串。
OK,现在业务代码有了,测试代码也已就位,数据库已运行,配置文件也在正确路径上,一切准备工作均已就绪,可以运行测试了。
Run测试代码,发现bug,修改业务代码,再次run,直到绿条出现,一个方法测试完成,一路有惊无险,很顺利。
当然上面的测试代码只测试了程序正常执行的分支,没有覆盖其它可能出现异常的分支,完全可以加入对异常分支的测试代码,不过这个业务比较简单,对正常分支测试通过基本就OK了。个人觉得,单元测试没有必要太看重测试覆盖率,够用就行了。
这样,测试类中就有了一个测试方法了:
java 代码
- public class EmpBOTest extends SpringUnitTest {
- protected EmpBOI empBO;
-
- public void setEmpBO(EmpBOI empBO) {
- this.empBO = empBO;
- }
-
- public void testSaveNewEmp(){
- CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm);
- fmCau.setCauGuid("9DDC036A177088F0FAE833CEA0971DF0");
- fmCau.setName("吕南");
- fmCau.setSex("02");
- fmCau.setBirth("1989-11-24");
- fmCau.setAddress("北京市大兴区");
- fmCau.setMobile("13810384254");
- fmCau.setWorkUnit("威天软件");
- fmCau.setTel("01055669235");
- ArrayList retlist = this.ccpUserBO.saveNewEmp(fmCau);
- assertEquals("true",(String)retlist.get(0));
- }
- }
测试代码运行后,数据库中就有了员工信息记录。接下来是实现员工信息的修改逻辑。测试代码如下:
java 代码
- public void testSaveEmp(){
- CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm();
- fmCau.setCauGuid("FD705E2FA08E95956241040BE3D83D69");
- fmCau.setName("吕南修改");
- fmCau.setSex("02");
- fmCau.setBirth("1968-10-15");
- fmCau.setAddress("北京市昌平区");
- fmCau.setMobile("13999999999");
- fmCau.setWorkUnit("join-cheer");
- fmCau.setTel("01058561199");
- ArrayList retlist = this.ccpUserBO.saveEmp(fmCau);
- assertEquals("true",(String)retlist.get(0));
- }
然后实现具体业务逻辑代码,运行测试,OK,绿条出现,测试通过。完事大吉了吗?
貌似没有问题,但是上面的测试代码实际是很脆弱。请注意这一句:
fmCau.setCauGuid("FD705E2FA08E95956241040BE3D83D69");
这是设置待修改的员工主键,问题就在这儿。主键是随机产生的,每个员工的主键值都是不同的。此处我将主键写死,该主键代表运行新增员工的测试代码testSaveNewEmp得到的员工记录。那么,我下次运行新增员工方法testSaveNewEmp时,得到的员工记录主键发生了变化,为了使修改员工的测试方法testSaveEmp正确运行,我发布修改测试方法中的代码,将待修改的员工对象主键值设为这次得到的员工记录的主键值。
如此一来,我的测试类无法运行,只能每次运行其中的一个测试方法。这种笨方法在业务逻辑开发阶段还能承受,但测试的自动化运行就没办法了。测试结果无法再现,每次运行都要修改测试代码,太恐怖了!
经过N久的痛苦之后,我决定改变测试基类。现在的基类扩展自AbstractDependencyInjectionSpringContextTests,我将其改为继承自AbstractTransactionalDataSourceSpringContextTests,这样我的测试类就自动具有事务功能,即在每个测试方法执行后Spring会自动回滚事务,将数据库还原为初始状态。然后在我的测试类里定义一个私有方法,用于向数据库中插入测试数据,每个测试方法运行时都要调用这个方法产生数据,因为测试类具有自动回滚功能,每个方法运行完成后,数据库会还原,将测试数据清空,以待下次运行时插入。这样一来,每次测试类运行时,可以保证测试数据都是相同的。这样我就可以随时运行测试而不用担心修改测试代码了。
修改后的测试基类如下:
java 代码
- public class SpringTransactUnitTest extends
- AbstractTransactionalDataSourceSpringContextTests {
-
-
-
-
- protected String[] getConfigLocations() {
-
- setAutowireMode(AUTOWIRE_BY_NAME);
- return new String[]{Constants.DEFAULT_SPRING_CONTEXT_HIB,
- Constants.DEFAULT_SPRING_CONTEXT_SER,
- Constants.DEFAULT_SPRING_CONTEXT_RES,
- Constants.DEFAULT_TEST_SPRING_CONTEXT_CCP,
- Constants.DEFAULT_TEST_SPRING_CONTEXT_APP
- };
- }
- }
修改后的测试类如下:
java 代码
- public class EmpBOTest extends SpringTransactUnitTest {
- protected EmpBOI empBO;
-
- public void setEmpBO(EmpBOI empBO) {
- this.empBO = empBO;
- }
-
- private ArrayList add(){
- CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm);
- fmCau.setCauGuid("9DDC036A177088F0FAE833CEA0971DF0");
- fmCau.setName("吕南");
- fmCau.setSex("02");
- fmCau.setBirth("1989-11-24");
- fmCau.setAddress("北京市大兴区");
- fmCau.setMobile("13810384254");
- fmCau.setWorkUnit("威天软件");
- fmCau.setTel("01055669235");
- ArrayList retlist = this.ccpUserBO.saveNewEmp(fmCau);
- return retlist;
- }
-
- public void testSaveNewEmp(){
- ArrayList retlist = add();
- assertEquals("true",(String)retlist.get(0));
- }
-
- public void testSaveEmp(){
- add();
- CcpAclsUserExtForm fmCau = new CcpAclsUserExtForm();
- fmCau.setCauGuid("9DDC036A177088F0FAE833CEA0971DF0");
- fmCau.setName("王小二");
- ArrayList retlist = this.ccpUserBO.saveEmp(fmCau);
- assertEquals("true",(String)retlist.get(0));
- }
- }
业务类中的删除与查询方法就不在此赘述了。
一个小技艺:私有方法add用于产生测试数据,其实也可能通过AbstractTransactionalDataSourceSpringContextTests提供的jdbcTemplate执行原生SQL语句来向数据库中插入数据。这在测试业务类没有提供新增方法时非常实用。
另外,我认为,测试时最好使用一个单独的测试库,不要用项目组公用的开发库,否则会产生很多麻烦。
以上是我在具体项目中实施单元测试的一点心得体会,贴出来给大家参考,也欢迎大家提出批评意见。