代码是为了什么,当然是为了重复运行。如何保持单元测试代码的稳定?主要靠好的API设计。API切实正确切割了需求,那么在重构的时候API就基本不用变化,测试用例也不用重写。以后你重构的时候,只要你的测试用例覆盖的够好,基本跑一遍就知道有没有改出问题。这样可以节省大量的时间。
从这段话里可以得到2个信息:
- 良好的API设计保证在很大程度上避免重写unit test
- 覆盖率良好的unit test可以让你重构代码的时候省心省力。
什么是单元测试
unit test is the idea that they are tests in isolation of individual components of software.
--Michael C. Feathers(修改代码的艺术作者)
软件工程术语 Unit testing 中的 Unit(单元)有多种含义,差不多就是程序模块(Module)的意思。 你的程序主要是由一个个的 Class 组成的,一个类或一个对象当然也是一个单元,而比类更小的单元是类的方法(函数)。如果你的类中的基本单元——如某些方法不能正常工作,在某些输入条件下会得出错误的执行结果,那么如何保证你的类/对象乃至整个应用软件或系统作为一个整体能正常工作呢?所以,简单说,单元测试(优先)的目的就是首先保证一个系统的基本组成单元、模块(如对象以及对象中的方法)能正常工作,这是一种分而治之中的 bottom-up 思想。
随着敏捷运动以及 Java 等新一代 OO 语言、技术和开发工具的兴起,自动单元测试在实践中得到了大面积普及,这主要还是得益于 Kent Beck、Erich Gamma 两位大师联合为 Eclipse IDE 开发的自动单元测试工具 JUnit 的一举成名,以及后来喷涌而出的庞大 xUnit 家族(其中就包括 cppUnit)。xUnit 系列工具其实都源于 1998 年 Kent Beck 为 SmallTalk 语言开发的自动单测工具 SUnit。
单元测试的价值
单元测试原则:
1. 测试名称要尽可能符合测试内容
每一个单元测试的测试用例名称要尽可能说明测试的内容,这样方便后续测试用例的审查,例如:
YES: def test_add_with_int_and_str(self)
NO: def test_add(self)
2. 非常简单的函数可以不写单元测试
为一个非常简单的函数去写单元测试很多时候会得不偿失,比如一个函数的逻辑就这样:
def add(x, y):
return x + y
为它写测试的价值就很低。
3. 控制函数的大小
每个函数都应该尽可能避免出现复杂逻辑,同时单个函数长度尽量控制在100行以内。 太长的函数阅读起来难度太高,写单元测试的难度更高。
4. 对全新或者修改过的代码进行单元测试
系统中已经在运行但是没有单元测试的代码,可以先不去管它(也可以烧烧香,拜托不出现故障) 当需要对这些代码进行修改时,那就应该先对这部分代码补充测试用例,如果单元测试不好写的话,请先补充功能测试。之后再去修改代码,在修改的过程中,对代码补充单元测试用例。 新添加的代码,需要有对应的测试用例。 这样才能让测试覆盖率不断上升。
5. 测试用例要作为代码文档的一部分
6. 不要单独为测试创建特别的逻辑或接口
测试用例测试的是正常的代码,如果对于测试创建特殊逻辑的话,就错误理解了测试用例存在的价值了。例如:
if ak == debug_ak:
do something
7. 测试尽量正交
正交是指两两测试用例之间没有逻辑上的重合,这样的测试用例集合可以达到最高的ROI。
8. 一个测试用例一个断言
好的测试用例里只应该有一个断言,例如:
YES: def test_a(self):
self.assertEqual(1, 1)
NO: def test_a(self):
self.assertEqual(1, 1)
self.assertEqual(2, 3)
这是因为当一个测试用例里的断言失败时,它会退出这个测试用例。如果这个测试用例后续还有别的断言的话,也不会被执行,这样会增加测试用例验证的时间成本。
9. 要删除掉dead case&消除重复case
dead case是已经无效、每次运行都会失败的测试用例,这些测试用例会影响每次的回归结果。 重复case会带来2方面问题:
- 每次回归测试都要执行,浪费回归测试的时间
- 一旦发生改动时,需要对每一个重复的case都进行相应修改,浪费时间成本。
10. 测试用例要尽可能能可重复性操作
测试用例可重复运行带来的好处是,测试的结果可以稳定复现,方便定位问题。例如: ··· def add(x, y): return x + Y + api_call(x, y)
def test(self): api_call = mock(api_call) api_call.return_value = 10 self.assertEqual(12, add(1, 1)) ··· 所以应该尽量用mock、stub而不是调用接口获取数据。
单元测试的收益(Return on interest)
书写单元测试的原则是:最小投入,最大产出 度量方法: 投入: 产品代码行数/测试代码行数 效果:覆盖率(函数覆盖率,条件覆盖率,语句覆盖率)
I get paid for code that works, not for tests. so my philosophy is to test as little as possible to reach a given level of confidence.
-- Kent Beck
测试覆盖:
Testing coverage(测试覆盖),指测试系统覆盖被测试系统的程度,一项给定测试或一组测试对某个给定系统或构件的所有指定测试用例进行处理所达到的程度。
函数覆盖
指设计足够的测试用例,使得被测试程序中每个函数至少被执行一次。
语句覆盖
测试时要覆盖到程序中的每一个语句。语句覆盖是指设计足够的测试用例,使被测试程序中每个语句至少执行一次。
判定覆盖
设计足够的测试用例,使得被测试程序中每个判断条件至少获得一次true和false。从而使得程序的每一个分支至少都通过一次,因此也被称为分支覆盖
条件覆盖
设计足够多的测试用例,使得判定表达式中每个条件的各种可能的值至少出现一次。
判定条件覆盖
设计足够的测试用例,使得判定表达式的每个条件的所有可能取值至少出现一次,并使每个判定表达式所有可能的结果也至少出现一次。
条件组合覆盖
设计足够的测试用例,使得没有判定表达式中条件的各种可能的值得组合都至少出现一次。
路径覆盖
设计足够的测试用例,覆盖被测试程序中所有可能的路径。
一般来说会采用条件组合覆盖为主设计测试用例,然后再补充部分用例,达到路径覆盖的标准。
怎样写更少的测试:
- 在API层次尽可能完整表达需求,在低层按需细化需求
- 大量细粒度测试对信心提升有限
- 通常在低层测试实现异常流程测试
- 单元测试可以避免90%的BUG,而不是100%
测试用例的书写顺序:
TDD & BDD
TDD和BDD都是目前比较常见的开发方式。
TDD: Testing Driven Development(测试驱动开发)
TDD的理念是,在开发具体代码之前,先设计测试用例。在开发代码的过程中尽量不修改已经设计好的测试用例,这样当所有的测试用例都通过时,新的功能也可以认为开发完毕。 TDD有一个比较好的例子是python koans,github地址为: python-koans
BDD: Behavior Driven Development(行为驱动开发)
行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。
常见的测试框架
我把Python的测试框架分为2类:
一类是正经的测试框架,这类框架是比较"严肃"的测试框架,比如unittest,它在其它语言中也有对应的实现,是业界的best practice。
另一类是不那么正经的测试框架,比如doctest和lettuce。像doctest是Python独有的测试用例的类型,它允许用户在docstring中写测试用例。而lettuce则更好玩一点,它允许用户用接近自然语言的方式去写功能测试。推荐大家去玩一下。