关于单元测试的一些好实践

关于单元测试的重要性就不做过多的赘述,其基础概念可以参考http://blog.csdn.net/linlinlinxi007/article/details/5294098


最近看到一个ppt介绍UT方面的best practice最佳实践,根据个人对UT的理解将其大致内容做一个简单的陈述。

首先我们在开发产品的时候Production Code和Testing Code这里指UT,都是位于代码级别的,也就是关注在底层的逻辑实现上面,但是二者关注点有所区别,

Production Code关注的是:

  • Meet business (functional) requirements
  • Meet non-functional (system) requirements
Testing Code关注的是:
  • Testing
  • Documentation
  • Specification
UT的Testing Code除了完成对于基本的测试的任务外,Documentation这样的功能也是非常重要的,它使得Testing Code自动成为了天然的Production Code的文档和注释,是可编译、可运行的,并且它保持最新,永远与代码同步,省去了以前在Production Code中添加注释及说明,但是重构后不适时更新造成的code与documentation不一致的情况发生。当然前提条件是Testing Code必须具有很高的可读性和测试性,这对于编写UT Code代码的程序员本身就是一大挑战。

正因为UT的Testing Code所focus的点与Production Code有所不同从而造成UT在编写方面所需要注意的地方与我们平常的Production Code存在很多的不同。Testing Code有着自己所特有的best practices。

5 steps
基本的UT的case都需要遵循一个5步的编写方针,那就是:
  • Setup
  • Prepare an input
  • Call a method
  • Check an output
  • Tear down
对于某一些资源无关的test case,setup和tear down不是必选项。这里Check an output不是说invoke的method一定是有返回值的,而只是说你的test case所测试的结果必须是可以感知到的,可以为class的field,也可以为alter string info等等。

Fast
运行的UT必须是快速执行的。因为UT是所有test里面执行频度最高的,在重构代码的过程中,新功能的开发过程中,随时随地都会运行,并且具有极高的回归测试性,能够最大限度的最为高效的保证Production Code的功能性。一旦其运行过慢势必会造成更少的被开发人员执行,更少的执行就表示粒度更大的功能性存在bug的可能性加大。有一个基础的数据指标作为参考:
Single test: <200ms
Small suite: <10s
All tests suite: <10min

Consistent
保持test case在任何时间任何环境下面的测试一致性在UT里面非常重要的。
Date currentDate = new Date();
random.nextInt(100);
这样的代码因为在每次测试的时候都会因为产生不同的值使得不可能做到consistent。解决Testing code的一致性问题的基本方式为Mock和DI,其中需要的核心在于解依赖技术的应用。


Atomic
原子性,对于test case运行的结果只有两种:pass和fail。不存在第三种情况,也就是说partially successfully tests是没有的。一个test case fail的话整个suite也都是fail的。用破窗理论来说,如果在suite里面存在某一个case fail,但是属于部分通过的话,接下来就会有第二个fail的case,直至整个suite都无法test。所以原子性是保证UT的可靠性的一大重要特性。

Single responsibility
单一职责,在Design Pattern中我们常常关于Production Code 的SRP,在UT的test case中也同样需要SRP。一个test case的职责是测试某一个behaviour,而不是method,如果一个method有多个behaviour,那么就需要对该method编写多个test case,如:
public String getXXXX(boolean flag){

   	 if(flag){
       	 	......
    	 }else{
       		 ....
    	 }
	}

public String getXXXX(boolean flag){

   	 if(flag){
       	 	......
    	 }else{
       		 ....
    	 }
	}
在上面的code中,getXXXX method有两个behaviour,那么对应的需要编写两个test case,如:

需要注意的是不同的behavior需要不同的test case进行测试,而不能将其写成同一个case里面,进行多次的判断。

Test isolation
test case间的无关性,也就是说不同的test case之间应该是没有任何关联,一个执行的成功与否不会影响另外的case的执行结果。无论test case运行的顺序是怎样的,得到的结果也应该是相同的。在UT的测试框架XUnit系列,就是提供了一个关注于5steps的执行方法做到test isolation的很好的实现。与其他的关注于更加高层的诸如TestNG等测试框架有着本质的区别。

Environment isolation
环境无关性,UT中应该杜绝所有外部环境的影响,诸如:database access,network,webservice call等等。


解决方案是使用Mock


使用Mock来解除对于Production Code 的环境依赖有以下几个方面的好处:
  • 不会在产品代码中产生额外的测试逻辑
  • 因为不需要构造依赖于环境的对象,能够做到快速测试的目的
  • 易于编写
  • 易于复用
而在java领域比较出名的Mock框架有EasyMock,JMock,Mockito等。

Classes isolation
classes之间的无关性,在很多时候我们产品代码中的很多class都是紧密耦合依赖于特定的class的实现,这样使得你无法在test中将其用mock对象进行模拟取代。如:
public String getXXX(){

    DB db = new DB();
    ......
    return db.getXXX();
}
这样的代码无论测试如何进行编写都会在测试过程中依赖于DB这个class的实现,假设DB是一个操作database的class,那么久使得该代码对于外部环境有着依赖。即使采用mock也无法避免。这样的代码因为对于特定的class无法做到隔离,所以也称之为不可测试的代码(此处指不可单元测试,集成测试是需要特定环境的支持的)。
可以借鉴的解决方案:
  • 不要在方法内部直接调用特定的class 的构造方法,改为通过工厂方法或者DI进行注入;
  • 通过接口编程,自顶向下的设计,做到IoC。
Fully automated
自动化,在单元测试执行的过程中是不应该出现人工干预的(No manual steps involved into testing)。在目前大多数的IDE中集成UT的framework都会有相应的自动化执行command,方便进行testing code的编写和执行。

Self-descriptive
自注释,单元测试应该是产品代码在开发层次的说明文档(development level documentation),同时也是与产品代码一同update的方法说明(method specification),虽然UT测试的是behavior,但是其测试的最基本实体还是class中的一个个method。因此要让UT的Testing code成为文档和说明,就必须让其易于阅读和理解,包括变量,方法,类的命名。

No conditional logic
测试代码中不应该出现条件判断逻辑。在UT测试代码中最好的编写方式是不会出现诸如:if和switch等关键字,意思就是所有需要测试的输入都是确定的,所测方法的行为也都是可预测的,执行的结果也都必须是strict的。当在testing code中出现需要通过if或者switch等判断的条件的时候就证明你需要将if else的分支分别通过两个independent的test case来分别进行测试。如下的测试代码:
testMethodBeforeOrAfter(){
...
if (before) {
assertTrue(behaviour1);
} else if (after) {
assertTrue(behaviour2);
} else { //now
assertTrue(behaviour3);
}
}
应该被替换为
testMethodBefore(){
before = true;
assertTrue(behaviour1);
}
testMethodAfter(){
after = true;
assertTrue(behaviour2);
}
testMethodNow(){
before = false;
after = false;
assertTrue(behaviour3);
}

No loops
不应该出现循环。编写的高质量测试UT代码是不应出现诸如“for”,“while”,“do-while”等循环关键字的。
关于在测试代码中可能出现的loops循环有以下三种情况:
  • Hundreds of repetitions
  • A few repetitions
  • Unknown number of repetitions
Hundreds of repetitions这种情况的出现就证明所编写的test coding过于复杂,同一个test case测试了不同的behavior,违背了SRP的原则,首先要做的就是化繁为简,将其进行拆分和简化。
A few repetitions,仅仅是出现a few repetitions是可以接受的,但是还是应该尽量将这几次循环代码逻辑通过非循环的代码清晰的表现出来(type the code explicitly without loops)。例如可以将需要循环的逻辑抽取到一个公用的method方法中,然后分别调用该方法。
Unknown number of repetitions,这种情况的出现已经代表的不太可能将loops循环通过code explicitly 的方式解决,因为编写的人员本身就不知道测试的运行到第是如何的。只能证明一个问题,测试编写本身存在问题,修改这样的问题同时使得你的测试的inputs是strict的,并且保证对于这样的inputs所对应的test outputs也一定是strict的。

No exception catching
不要捕获测试代码中期待之外的异常。这条practice有一些双关的语义位于其中(No exception catching表示的不是不存在异常捕获的情况,而是非你所期待的exception出现的时候,也就是捕获exception的时候出现的其他exception你是不应该捕获的)。
  • Catch an exception only if it's expected
  • Catch only expected type of an exception
  • Catch expected exception and call "fail" method
  • Let other exceptions go uncatched
testThrowingMyException(){
try {
myMethod(param);
fail("MyException expected");
} catch(MyException ex) {
//OK
}
}

Assertions
进行断言判断。在test case的判断的时候尽量不要使用“==”,“equals”等与语义相关的关键字,而应该尽量采用断言的方式。
  • 最好是采用所运行的test framework提供的各种断言方法。
  • 对一些判断条件比较复杂的情况,编写针对该种情况的专有断言方法。
  • test suite中复用你所编写的assert method。
  • 在断言方法中进行loops判断,而不是loops中调用断言方法进行判断。
Informative assertion messages
断言中应该含有更多的信息。从断言失败中所提示的信息中,任何人都能轻易的明白是何故障导致断言失败。断言信息能够提供更好的产品代码的说明,另外一个方面也能够使得断言失败的信息更加清晰明白的传递给运行测试的人员。

No test logic in production code
产品代码中不应该出现测试逻辑。这里分为两个层次,其一为测试代码本身应该和产品代码区分开,另外一个更加底层的是产品代码本身的逻辑中不应该出现因为迎合测试而编写的逻辑分支。对于第一点做到不难,但是要在产品代码层次完全隔离而不出现专门为测试所编写的逻辑分支确不太容易(至少我是这么认为的)。
  • 分离测试代码与产品代码
  • 不要创建仅仅是为测试而存在的方法和成员变量
  • 通过DI依赖注入使得代码的接缝清晰
  • 尽量少的在产品代码中出现为测试逻辑而写的分支(完全避免几乎不可能)

Separation per type
区分不同种类的测试,如UT和integration tests。不要把UT和其他种类的tests混淆。必须从以下几个方面了解其区别:
  • Different purpose of execution
  • Different frequency of execution
  • Different time of execution
  • Different action in case of failure
无论是什么样的测试,UT永远都是关注的最为底层的东西,是同产品代码打交道最为直接的测试,所以UT在编织安全网的同时也是最快能够发现bug的测试。因此编写好的UT代码从各方面来看都是有百益而无一弊的,尽管很多人会认为编写test非常耗费时间,同时一些代码难于编写test case,这只能说你的产品代码环境依赖性太强或者class依赖太强,你一方面需要解除这方面的依赖关系,另外一个方面没有能够体会到UT所带来的真正的好处。尤其是对那些业务需求经常变化,或者经常重构的产品来说尤为重要。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值