Categories
程式開發

测试用例最佳实践


申明:本文我列举了一系列我认为比较实用的测试用例编写实践。我之所以称其为最佳实践,是因为这些技巧帮助我写出易读,可维护以及能更好描述业务逻辑的优质测试用例。这些最佳实践可能略显主观,你可能有其他更好的建议,这样很好,请不要犹豫把你的观点在评论区分享给大家。

开发的过程中测试用例扮演的角色

在开发过程中,测试用例扮演了很重要的角色。它给你很多好处:

测试用例验证需求。它将让你时刻警惕,你是否在正确的实现业务需求让你尽早暴露缺陷。越早暴露问题,解决的成本就越低速度越快,而测试用例可以让你在开发过程中抓住这样的时机。改善可维护性。写测试用例的前提是代码本身可测试,这意味着你首先得想办法保证你的代码是可维护的。可测试的代码一般是解耦做的比较好,这中代码具备可读性,也具备更好的结构。让你放心重构。完善的测试用例保障大的改动也不会导致引发回归测试。帮助code review。因为测试用例通常表达了开发者的意图,reviewer很容易就可以透过测试用例看出程序的深沉逻辑。

什么是好的测试用例

我们首先来定义一下什么才是好的“测试用例”

通常,一个号的测试用例具备如下特点:

可被信任。这意味着一个测试用例如果跑不通,就一定表示程序出问题。如果测试用例是否通过跟代码逻辑正确性没有必然联系,那这个测试用例就不能被信任。易读/易维护。你应该能很容易透过测试用例看出它在测试什么以及怎么测的,而不会被一些很隐晦的引用或者状态控制所干扰,这表明一个好的测试是直观且内聚的。一个测试用例只验证一个case。这个涉及到单一原则。如果一个测试用例内assert很多的case,那么当它失败的时候,你就无法立刻知道到底哪里出问题了。独立性。一个测试用例不应该影响其他的测试用例。一个典型的例子是很多测试用例共享一个全局变量,如果是这样,那么跑测试用例的顺序将影响测试的结果,此时你就得花更多的精力去搞清楚到底发生了什么。

测试过程的最佳实践

第二部分,我们将探讨什么是一个好的测试过程?

一个好的测试过程具备如下特点:

它应该是自动化的(比如CI)

当测试用例能在对的时机及时执行时,它才是有意义的。最好是持续集成,在这个过程中,你的测试会在某个时机被经常执行,比如在每次提交的时候。不然,你很可能会忘记跑测试用例,也就形同虚设。

测试用例应该在开发的过程中写,而不是之后

TDD(先写测试,再写业务逻辑)是不错的,但是你不一定能在一开始就能看穿一个模块应该是什么样,类结构应该如何设计等等。所以,即使你不能在一开始就写测试用例,我觉得ok,其中的原则是我们要尽早而不是拖到最后写测试用例。

之所以要坚持这个原则,是因为测试用例能帮助我们写出干净的代码,包括分离关注点,使用接口隐藏具体实现细节。如果延迟写测试用例,你将会陷入围绕不可测试的代码各种hack的境地,而且身不由己,无法自拔。

为缺陷以及业务单元增加测试用例

你大可不必为所有理论上可能逻辑分支写测试用例(关于这一点,下面会继续聊)。

重要的是,你的测试用例必须能很好的表达业务逻辑单元,以及在后续的需求增加以及变更,还有缺陷发生时,你的测试用例都应该有相应的体现。尤其要强调针对Bug写好相应的测试用例,这种积累很大程度上可以保证你的软件是正向发展的。

测试的最佳实践

接下来我想跟大家分享更多可以让给你的测试用变的更好的通用理论实践。

用心描述你的测试用例

针对你的每个测试用例,你应该描述清楚它在测什么?被测试内容的场景是什么?以及测试期望的结果是什么?

如果针对某一个测试想到一个新的用例,那可以把加到测试文档中去。如果测试描述太长,可以用缩写,并在测试文档中描述缩写的含义。

一个不好的测试描述将降低可维护性。

测试公开的接口

所有不公开的代码,都不应该被测试。

不要通过打破代码的封装性来测试某一个功能点。

如果你将要单独测试的某个方法,这意味着这个方法很可能应该是归属于某一个接口的实现类中,可以是工具方法或者扩展。

每一个类都必须有一些对外接口,来清楚的表达什么应该被测试。

测试一个类的私有部分,会让测试用例更加难以维护,破坏封装性等于让一个良好结构变成废墟。

在一个测试中只验证一个case

一个测试用例应该只测试一个点,也就是说每一个测试用例应该只有一个assertion。这样做的话,当该测试用例没跑通时,我们就很快知道哪里出问题了。

但在做assertion之前,我们往往需要检查若干个点。比如我们想知道在一个mock对象中,什么方法被调用了,那么此时写多个这样的检查时可以被接受的。

一个测试用例要有良好的结构

在一个简单测试用例中(仅仅是测试返回值),其结构分为“设置”和“返回值验证”两部分。

在一个复杂测试用例中(可能已经接近集成测试),其结构应该分为“设置”,“触发”(什么时候或场景下触发),“结果”。

这样的结构可以让测试用例更加可读以及可维护。

使用依赖注入

通过构造器或者公开接口为待测试类设置依赖,而不是在类的内部通过new的方式创建依赖。不要从类内部获取单例实例。

用接口封装一些系统或平台的类,然后其他系统去依赖这些接口,依赖注入的设计方式可以增加代码的可测试性。

Mocks vs Stubs

尽可能的使用真实的类。如果实在不行,在考虑用Stub(存根),如果还不行,那在考虑用mock。

给实体或值对象设的值应该是真实的,直接相关的服务应该是真实的,或者是存根过的,第三方的依赖服务应该是存根过或者mock的。

在测试中过度的使用mock可能会导致测试结果失真,从而失去了测试的效果。

实体或值对象的默认构造器

当一个类被作为实体或者值对象时,如果有builders去构造默认值将是一个非常方便的事情。

通常来说,这应该是一个实体或者值对象中的默认实现,可以去修改对象中的属性对测试用例来说非常重要。如果没有这样的builder将会导致重复代码,测试用例也不好维护。

测试类组合

创建一个包含一些通用设置的基类,然后通过继承它并创建子类的方式来测试功能的某一部分。

这样可以将相关的测试类紧密的捆在一起,而且这种方式可以更好的组织测试用例的名称,比如讲测试用例描述中的通用部分提取出来。

把所有的测试用例扁平化的放在一个类里面会降低可读性。

总结

测试用例真的不好写,这需要纪律。

而且,测试用例也是代码,是代码就应该用生产的标准去要求它。当你投入足够时间到单元测试,你也会从它身上得到越来越多的好处。

不要害怕写单元测试,不要等待,今天就开始,持续地在单元测试上做出努力。

快乐工作,快乐生活!

原文:Unit Testing Best Practices

题图:卡瓦格博峰