见微智著 - 从一个小例子看Python中的单元测试

 Python
 

单元测试

据统计,由于软件缺陷(bug),美国经济每年在浪费生产力、返工和实际毁坏上损失了数十亿美元。近期最严重的案例是波音737 Max飞机的两次重大坠机事故,共造成了346人死亡。经过初步调查,该公司的专用软件难辞其咎。因此,通过软件工程方法以及测试减少软件的缺陷,十分重要。

版权声明

本文可以在互联网上自由转载,但必须:注明出处(作者:海洋饼干叔叔)并包含指向本页面的链接。

本文不可以以纸质出版为目的进行改编、摘抄。

代码错误或者软件bug和程序员如影随形。作为程序员,我们经常在担心:“我编写的代码是否正确?我编写的代码有没有bug?”。测试驱动开发提供的测试集可以减轻你的担心。

1. 测试驱动开发

测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它的基本思路就是通过测试来推动整个开发的进行。在明确要开发某个功能后,不是直接实现功能部分,而是先思考如何对这个功能进行测试,先完成测试代码的编写,然后再编写相关的功能代码满足这些测试用例。

测试驱动开发的基本过程如下:a. 先明确要实现的功能并进行分析;b. 为该功能的测试设计用例并编写测试代码,并在测试用例设计过程中检视该功能的接口;c. 编写代码实现功能;d. 使用测试用例进行测试;e. 如果测试没有通过,对代码进行重构,直至测试通过为止;f. 使用相同流程循环完成软件其它功能的开发。

读者也可能听说过一种叫做“极限编程”的轻量级软件工程思想。在“极限编程”里,一个重要的规则就是:先写测试,这跟TDD异曲同工。测试驱动开发的一个重要实施手段就是单元测试。

2. 单元测试

单元测试是保证软件模块质量的重要手段之一,通过单元测试来管理设计好的测试用例,不仅可以避免测试过程中的人工输入引起的错误,还可以重复利用设计好的测试用例。Python标准库unittest提供了很多用于单元测试的类和方法,其中最常用的是TestCase类,其常用方法如表所示。

名称 功能 名称 功能
assertEqual(a, b) a == b assertIsNone(x) x is None
assertNotEqual(a, b) a != b assertIsNotNone(x) x is not None
assertTrue(x) bool(x) is True assertIn(a, b) a in b
assertFalse(x) bool(x) is False assertNotIn(a, b) a not in b
assertIs(a, b) a is b assertIsInstance(a, b) isinstance(a, b)
assertIsNot(a, b) a is not b assertNotIsInstance(a, b) not isinstance(a, b)
assertAlmostEqual(a, b) round(a-b, 7) == 0 assertNotAlmostEqual(a, b) round(a-b, 7) != 0
assertGreater(a, b) a > b assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b assertLessEqual(a, b) a <= b
assertRegex(s, r) r.search(s) assertNotRegex(s, r) not r.search(s)

基于测试驱动开发的思想,我们编写一个类来测试判断素数的函数。首先,我们定义了该素数判断函数的接口:函数名为isPrime,接受一个整数参数num, 如果num是素数,该函数返回True, 否则返回False。该函数的接口可以使用伪代码表示如下。

1
bool isPrime(int num)

根据TDD, 先写测试,测试类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#testisprime.py
from prime import isPrime
import unittest
class IsPrimeTestCase(unittest.TestCase):
def testIsPrime(self):
self.assertEqual(isPrime(2),True,'素数判断错误')
self.assertEqual(isPrime(7),True,'素数判断错误')
self.assertEqual(isPrime(12),False,'12不是素数,判断错误')
self.assertEqual(isPrime(0),False,'0不是素数,判断错误')
self.assertEqual(isPrime(1),False,'1不是素数,判断错误')
self.assertEqual(isPrime(-7),False,'负数不是素数')

if __name__ == '__main__':
unittest.main()

我们看到,测试类中把2,7,12以及比较特殊的0,1等作为参数,交给isPrime(num)函数进行判断,并把返回值与预期的值进行比较。如果返回值与预期值不一致,则会导致断言失败,这个失败的断言还会附带一个说明字符串帮助测试者确定测试未通过的原因。

接下来,根据需求以及在测试用例设计过程中检视过的功能接口实现isPrime()函数,版本为0.1:

1
2
3
4
5
6
#prime.py  v0.1
def isPrime(num):
for i in range(2,num):
if num % i == 0:
return False
return True

接下来,运行testisprime.py进行测试,得到如下错误结果:

1
2
3
4
5
6
7
8
9
10
11
12
======================================================================
FAIL: testIsPrime (__main__.IsPrimeTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "d:\pylearn\C12_UnitTestException\testisprime.py", line 9, in testIsPrime
self.assertEqual(isPrime(0),False,'0不是素数,判断错误')
AssertionError: True != False : 0不是素数,判断错误

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

该错误结果表明,测试没有通过,这说明0.1版本的功能代码存在缺陷。仔细查看,发现对值0的素数判断结果真,而期望值应为假。重新检视isPrime(num)函数的设计,发现该函数遗漏了对值0和1的处理,进行修改,得到prime.py的0.2版本:

1
2
3
4
5
6
7
8
#prime.py  v0.2
def isPrime(num):
if num in (0,1):
return False
for i in range(2,num):
if num % i == 0:
return False
return True

再次运行testisprime.py进行测试,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
======================================================================
FAIL: testIsPrime (__main__.IsPrimeTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "d:\pylearn\C12_UnitTestException\testisprime.py", line 11, in testIsPrime
self.assertEqual(isPrime(-7),False,'负数不是素数')
AssertionError: True != False : 负数不是素数

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

又没有通过测试,这次是因为num为负数的情况没有得到处理。审视并修改prime.py,得到0.3版本:

1
2
3
4
5
6
7
8
#prime.py  v0.3
def isPrime(num):
if num < 0 or num in (0,1):
return False
for i in range(2,num):
if num % i == 0:
return False
return True

再次运行测试,结果如下:

1
2
3
4
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

这次通过了,如果我们的测试用例设计得充分而又合理,可以认为isPrime(num)函数“合格”了。prime.py的版本号可以从0.3直升至1.0了。

上面这个极其微小的例子是希望浅显地向读者介绍单元测试的基本过程,真实的情况要复杂得多。几乎很少有程序员可以一次性地写出没有缺陷或者极少缺陷的代码,程序总是在不断修正,重构中提升其质量。除了写代码外,如何设计合理有效的测试用例,也是一项专门的学问,它属于软件工程的范畴。读者大致可以看到,对于isPrime(num)这样一个简单的函数,测试用例测试了边界值(0,1),正数,负数等各种情况,试图覆盖isPrime(num)函数全部可能的执行路径。


 评论