【IT168 技术文章】
最近参与的一个项目里我把单元测试放到很重要的位置并且也发现了一些问题。顺便整理一下。
这不是一篇严谨的技术文章。只是一些个人不成熟的感想。
在实际开发过程中,我发现在单元测试代码中经常会出现两种情况:第一种就是在测试代码中炫耀编程技巧,第二种就是敷衍了事,你不是让我通过测试么?好,我就写一个用例,一定能通过的那种,然后告诉你,OK,我的测试通过了。我觉得,这就是对单元测试的意义没有真正理解的表现。
到底单元测试是做什么用的?我想,在说明这个问题之前,我先说说我所理解的测试到底是做什么用的。
所谓的测试,是一种产品质量保证的手段。我按照需求规格说明书制造了一件产品,那么谁来确保这个产品符合了需求规格的要求呢?就是测试。它会根据需求规格说明书设计一系列的场景和用例,来对产品进行测试,看看产品是不是真的符合所期望的需求。
要达到这个目标,其实并不十分的容易,因为一个真正的系统,情况十分复杂,里面充满了数不清的分支、异常、边界条件,甚至运行环境,将这些东西组合起来,产生的需要测试的点将会是一个天文数字,在有限的时间内做完一个充分而可靠的测试,是不可能的。
为了将充分测试变得可能,一个比较好的途径就是分层测试。我在做运行测试或性能测试的时候,有一个前提,就是假设整个系统的集成运行已经没有问题了,在运行测试或性能测试时,我将不再考虑“系统无法正常运行”这种场景。那么如何保证集成运行没问题呢?我们用集成测试来检验。但是在做集成测试的时候,我们同样要基于一个假定,就是各个模块的功能都能够如期正常工作。而这一点,又是通过模块自身的功能测试来完成的。……这样一层层往下推,每个层次就假设它所依赖的层次没有问题,这样就可以减少很多场景以及由这些场景引出的额外的分支。将原先一个几何级数的测试用例分解成可以接受的若干层次的算术级数的用例。这样一来测试就变得有可能做好了。
而单元测试,正是这些测试的最低层次——保证每个函数/方法,或者说最小功能模块的正确性的一种测试。
通过上面的描述,我们至少清楚了这样几件事情:1. 单元测试是一种测试,它不是代码的一部分;2. 单元测试是最低层级的测试,它只保证函数的可靠性,不保证其它;3. 单元测试应该能保证每一个函数的可靠性。
单元测试是一种测试,所以,我们应该以一种测试的眼光去面对它——我们要测试正常情况,边界条件,要对它的测试目标——函数做黑盒分析,白盒分析,选择合适的测试数据,构建测试场景和测试环境——总之,一切测试应该做的事情,单元测试都不应该省略。
理论上来说,单元测试和其他测试一样,也是可以纯手工完成的:我们可以写一段某函数的测试代码,然后输入我们的测试输入,观察测试输出,并跟期望值做比较——事实上这种人工测试,写了一段时间代码的人应该都不会陌生。但是,单元测试有一点特殊性,就是在一个系统中,函数会非常非常的多,变化也比软件的功能频繁的多。面对这么多的函数,这么频繁的变化,纯手工测试是不现实的。所以,我们必须要引入单元测试框架进行自动化测试。注意,这里的单元测试框架只是实现自动化测试的一个手段,对单元测试本身并不产生任何影响——没有单元测试框架,单元测试一样也是可以进行的,只是会痛苦很多。
单元测试框架引入的目的只是为了自动化单元测试,简化单元测试的步骤。所以,对于测试代码的编写,我们的重点应该是:1、如何搭建测试环境、测试场景;2、如何选择测试用例;3、如何校验测试结果。对于测试代码本身,应该尽可能的简单,能不要使用技巧尽量不要使用,我们的目的在于测试,如果测试本身过于复杂,我们不能保证测试的正确性,测试这个工作就白做了。
另外,刚刚提到单元测试是对函数的测试,因此,测试必须是以函数为单位的。每个函数应该拥有自己单独的一个测试,但是在这个测试中,我们应该针对这个函数的各个方面:正常的、异常的、边界的……等等,各个方面进行完善的测试,这样我们才能保证这个函数的功能是如我们所愿的。但是单元测试不需要负责函数的组合工作情况。那应该是(低层次)功能测试的工作,而不是单元测试的工作。这个功能测试就是在假定所有函数都工作正常的基础之上,对这些函数组合形成的功能模块进行测试。这种测试,视情况而定,可以使用单元测试框架,也可以使用其他自动化测试方法或者甚至是使用纯人工测试。
另外,我还想讨论一下单元测试的编写和运行。
绝大部分时候,单元测试的编写,是由开发人员做的。我们在以前某次对单元测试的讨论中,甚至有人认为,单元测试必须由开发人员完成,而不应该由独立的测试人员完成。对于这个问题,我是这样看的:测试是一种针对需求的验证工作。如果这个需求非常清晰,清晰到开发人员之外的人都可以轻易掌握(有些日本外包发出来的函数说明书就能达到这一点),这时单元测试可以由独立的测试人员完成。但是大部分情况下对于函数级别,做不到这一点。这时最清楚函数需求的人就是开发人员本人,在这种情况下当然就应该是开发人员自己编写测试用例。但是开发人员必须搞清楚自己身兼两个不同的角色:运动员(实现代码)和裁判员(检验代码),在编写测试用例的时候绝不能假定任何函数的实现,而应该完全按照它应该有的需求来做。这样才能做好单元测试这件事。很多时候单元测试形同虚设,就是因为开发人员没有很好的转换自己的角色造成的。
单元测试的运行,目前我们这个Python的项目比较容易,直接运行模块就是该模块的单元测试,而以模块形式import就是实际使用。对于像C++或者其他的一些语言来说,可能没有这样方便的形式。我们可以把测试写在独立的文件中,然后用makefile组合不同的项目和主函数来做到这一点。另外还有一点就是,实际运行过程中可能会有一些环境,这些环境在测试时难以获得,或者增加上去之后,就难以测试(比如网络环境、数据库环境等等),这时我们可以采用一些虚拟的环境来做到。我们把运行时需要的环境做一个简化的虚拟版本,然后以这个版本作为测试环境进行测试,对于Python来说,我们可以实现这样的一个库在测试时import进来并且同时做一些环境初始化工作,在C++里,我们可以专门为测试写一些运行库,在实际运行编译和测试编译时,链接不同的库。这在自动化测试技术中有个专门的名称叫做 Mock Object。关于这个,我就不再深入了。