单元测试强化之道
Author :Young Jan
一、 引言
我一直在思考,怎样把单元测试完美的覆盖到项目中,解决项目中碰到的一些问题。现在的项目中单元测试太独立,单兵奋战,不系统,能够为项目提供的支持实在有限,只能为开发人员提供一些开发时的辅助,但实际上单元测试可以做的事情是很多的,只不过需要考虑一下如何将单元测试很好的与项目契合在一起的问题,就能够发挥其强大的力量。
都知道单元测试可以强化整体代码的质量,但是一般项目中,开发人员都不愿意去做单元测试,大体有以下的一些原因:
l 工作量太大,可能会得不偿失。
l 工期太紧,正常功能完成都有问题,没有时间做。
l 需要花费时间学习单元测试工具,掌握单元测试的微妙技巧。
所以单元测试能够完美实施的概率,跟找到自己完美的伴侣概率一样低。可能需要天时:有严格的项目流程,有非常充裕的时间;地利:有能够实施自动化单元测试的能力;人和:开发人员为了追求自己极致开发质量而充分支持单元测试。
在这里我不去讨论如何达成以上目标,而是讨论在项目过程中,怎样更合理的引入单元测试,怎样让单元测试自然而然的融合到项目里。
接下来我会描述一些关于自动化单元测试的核心技术点,以及阐述单元测试和详细设计文档两者结合后,如何为项目带来更高的质量。
二、 测试问题思考
在我们实际项目中,经常会碰到以下测试方面的问题,常常令开发人员和测试人员都很头疼:
l 开发人员不进行测试就直接交付功能给测试人员进行测试。
l 多次迭代开发后,新修改的问题引发老的功能模块无法正常运行。
l 在集成测试时开发人员交付版本之前大规模内部测试耗时耗力。
这些问题都值得我们去思考,其实有很多的项目管理方法论已经系统的描述了这些问题的解决方案,不过要将其落实下来,还需要结合实际情况,根据自身能力来选择适合项目的方案来实施。而我在这里介绍使用单元测试来完美的解决它们的方案。
三、 单元测试与自动化测试
正统的单元测试,是在代码这个层次进行的底层测试工作,以便检验代码是否符合功能要求。自动化测试一般指软件测试工作的自动化,在这里,我们用来特指执行单元测试,并生成执行结果分析报告的整个过程自动化。
本文中,不过多描述单元测试的过程和具体细节技巧,只大概的提及一些重点和容易被忽视的地方:
l 功能模块需要多个单元测试,每个单元测试检验一部分代码逻辑。
l 功能模块的单元测试中,必须包含对主要业务点,以及重要逻辑功能的测试。俗称核心覆盖率。
l 单元测试需要可以进行集成整合,自动化的进行全套测试,并自动生成最后报告,清晰的看出哪个单元测试出问题了。
l 单元测试需要随着代码的变化同步重构,当某功能模块发生变更时,相应的单元测试也需要跟着变更。
自动化测试需要工具配合,有各种商业软件提供了这样的解决方案:提供某些测试用函数,支持各种语言,需要编写各种脚本语言以完成自动化测试工作。这无疑需要巨大的人力资源投入,需要专业的自动化测试人员。而草根级别可以采用轻量级的单元测试整合到自动化工具里的方案来实现自动化测试的过程,如Java领域的JUnit和Ant的整合。
四、 JUnit自动化测试实战
为避免本文过于理论化,本节我针对Java单元测试工具JUnit及Ant的整合来完成自动化单元测试的整个过程进行核心技术细节的描述。
Setup1 首先,我们先来描述和现实一套业务逻辑,银行存取款系统的功能,存款,取款,查询:
// 模拟银行账户的功能。 public class Account { public double balance; //存款余额 ,忽略getter/setter
public double minBalance; //保留余额,忽略getter/setter
// 存款,返回存款后余额。 public double save(double cash) { this.balance += cash; return this.balance; }
// 取款,返回取款后余额;如果剩余金额少于保留金额,则抛出异常。 public double get(double cash) throws Exception { if((balance - minBalance) >= cash) { balance -= cash; return query(); }else throw new Exception("至少保留余额:["+this.minBalance+"]元。"); }
//查询余额 ,返回当前余额。 public double query() { return this.balance; } } |
Setup2 然后,我们编写三个单元测试用例来对业务逻辑进行验证:
public class TestSave extends TestCase { //测试存款的测试用例 public void testSaveCash() { Account account = new Account(); //初始化需要被测试的功能 account.balance = 1000; //模拟初始数据,账户中有1000大洋。 double currentBalance = account.save(2000); //执行业务逻辑 assertEquals(account.balance, currentBalance); //验证结果 } } |
public class TestGet extends TestCase { //测试取款的业务逻辑 public void testGetCash() throws Exception { Account account = new Account(); //初始化需要被测试的功能 account.balance = 1000; //模拟初始数据,账户中有1000大洋。 account.minBalance = 300; //模拟初始数据,如果取款操作会造成余额小于300,则不允许取款。 double currentBalance = account.get(500); //进行取款业务逻辑操作。 assertEquals(account.balance, currentBalance); //验证结果,断言余额一致。
boolean faild = true; //用来标示取款是否成功的标志位。 try{ currentBalance = account.get(500); //再次进行取款业务逻辑操作。 }catch(Exception e) { faild = false; //由于此次取款,必然造成余额小于300,所以程序会抛出异常。 } assertEquals(false, faild); //断言取款会失败。 } } |
public class TestQuery extends TestCase { //测试查询余额的方法。 public void testQuery() throws Exception { Account account = new Account(); //初始化需要被测试的功能 account.balance = 1000; //模拟初始数据,账户中有1000大洋。
account.save(1000); //先存入1000。 account.get(1500); // 再取出1500。
assertEquals(500d, account.query()); //断言此时应该剩余500的余额。 } } |
将三个用例分开成三个类是为了演示接下来整合到一起进行自动化测试,实际情况中,针对一个功能逻辑的测试用例都在一个测试用例的类中。
Setup3 整合Ant,编写如下build.xml文件:
<?xml version="1.0" encoding="GBK"?> <project name="test-project" default="junit" basedir="."> <property name="src" value="${basedir}/src" /> <property name="dest" value="${basedir}/bin" /> <property name="report" value="${basedir}/report" /> <property name="lib" value="${basedir}/lib" /> <target name="junit"> <junit printsummary="true"> <classpath> <pathelement path="${dest}"/> <fileset dir="${lib}"> <include name="**/*.jar"/> </fileset> </classpath> <formatter type="xml"/> <batchtest todir="${report}" fork="on" > <fileset dir="${dest}"> <include name="**/Test*.*"/> <!-- 执行以Test开头的单元测试类 --> </fileset> </batchtest> </junit> <junitreport todir="${report}"> <fileset dir="${report}"> <include name="TEST-*.xml"/> </fileset> <report format="frames" todir="${report}"/> </junitreport> </target> </project> |
原理就是让Ant去执行所有的单元测试用例,并且自动生成Html格式的报告文件,生成的结果在目录report中。
Setup4 运行Ant并查看结果:
运行build.xml后,在结果目录report中找到index.html文件并打开,可以看到如下的结果:
该结果表明所有的用例都执行成功,假设取款测试用例没有成功的话,会看到如下结果:
也可以从一个全局的角度来看待测试用例的数量, 通过率等信息,如下图所示:
具体的细节需要各位在实施的过程中慢慢去掌握,重点已经全部在上面描述了。如果对某一部分的细节不明白或者需要更深的研究,可以查询该技术细节相应的文档。
在这里我们可以看出,与Ant整合后,JUnit的表达能力得到了大幅度的提升,可以完整的描述整个测试的过程,发生的异常,多少用例通过或失败,在理想化的项目中, 如果单元测试做的好,项目经理或者QA只需要定期执行Ant,分析结果,就能够了解项目的开发情况,避免出现质量管理与开发脱节的情况。而在集成测试交付版本之前,只需要执行Ant,就能够保证核心的功能模块没有出现问题,相当于开发人员从头到尾做了一次自测试。
五、 详细设计与单元测试的融合
以上都是进行单元测试必备的基础知识,但是在实施的过程中,会发现效果并不尽人意,就算满足了天时地利人和,也会出现开发人员漏写测试用例,测试用例编写不符合初衷,往往到后期会越来越混乱, 而这一切都是因为行动缺乏规划导致,每一次行动如果都有详细的规划,就不会偏移大方向,下面我介绍将单元测试用例整合进详细设计文档的方案。
详细设计文档都是万变不离其宗,模块化的方式详细描述每一个功能的逻辑和实现过程等,例如以下是某详细设计文档中某模块的结构:
1 . 功能描述 …… 2 . 数据结构 …… 3 . 应用逻辑 …… 4 . 接口与实现 …… 5 . 其他说明 …… |
而我们要做的就是,修改该结构,增加6 . 单元测试用例 ,在详细设计阶段,接口与实现完成后,就需要根据接口和实现,完成单元测试用例的编写,此处单元测试用例应当遵循以下基本的标准:
l 从验证功能实现是否正确的角度来编写单元测试用例,阐明应该输入数据、依赖的外部接口和预期的输出结果。
l 充分考虑功能点需要什么样的测试用例,这里的要点是:在精不在多;覆盖核心业务逻辑;照顾易发生错误的问题点。
l 详细描述单元测试内部执行逻辑,例如怎样创建被测试对象,怎样创建模拟数据,怎样创建模拟外部接口,怎样组合并进行详细的单元测试。
l 根据对项目质量要求高低以及功能模块不同的侧重面来决定是否设计反用例测试、容错性测试、完整性测试、边界值测试、压力测试等等。
在单元测试与详细设计文档结合之后,单元测试的设计和实现的工作被平均分摊在不同的阶段,设计阶段完成思考用例和设计的工作,开发阶段在进行代码编写时,同时进行单元测试的编写,而此时的单元测试已经有明确的目的,详细的步骤,落实到书面,经过了评审,开发人员不会觉得单元测试很空泛,按照用例的思路编写出单元测试即可,这样就能做到单元测试完全无缝的整合到项目中来。
六、 总结
在介绍完以上的方案之后,如果你没有看懂我要说什么,没关系,看下面几点就行了:
l 在详细设计阶段强制整合单元测试,提高开发人员对质量控制的认识,提高代码的质量。
l 在开发过程中,不断的整合单元测试,使其能够自动化运行全套测试用例。
l 定期进行全套单元测试用例的自动化测试,可以直观的发现哪些模块当前存在的问题,可以迅速找出由于新的修改导致了哪些老功能无法正常运行。
l 在集成测试交付之前,统一运行全套单元测试用例的自动化测试,可以迅速的回归测试以前的模块,在开发和测试之间介入一层保障机制,减少双方工作量。