浅议Python unittest测试单元

Python UnitTest 官方文档 敬上,在没有Debug环境的情况下,UnitTest简直就是无敌的存在了,报错清晰,言简意赅,便于迅速定位错误。

单元测试的三种测试样例

完整的单元测试样例包含功能测试、边界测试、负面测试三种类型。

完整的单元测试才能孕育出完整的代码。

  • 普通功能测试:我们首先要保证写出的代码能够完成面试官要求的基本功能。比如面试题要求完成的功能是把字符串转换成整数,我们就可以考虑输入字符串”123”来测试自己写的代码。这里要把零、正数和负数都考虑进去。在考虑功能测试的时候,我们要尽量突破常规思维的限制。面试的时候我们经常受到惯性思维的限制,从而看不到更多的功能需求。比如面试题17“打印从1到最大的n位数”,很多人觉得这道题很简单。最大的3位数是999、最大的4位数是9999,这些数字很容易就能算出来。但是最大的n位数都能用int型表示吗?超出int的范围我们可以考虑long long类型,超出long long能够表示的范围呢?面试官是不是要求考虑任意大的数字?如果面试官确认题目要求的是任意大的数字,那么这道题目就是一个大数问题,此时我们需要特殊的数据结构来表示数字,比如用字符串或者数组来表示大的数字,以确保不会溢出。
  • 边界值的测试:很多时候我们的代码中都会有循环或者递归。如果我们的代码基于循环,那么结束循环的边界条件是否正确?如果基于递归,那么递归终止的边界值是否正确?这些都是边界测试时要考虑的用例。还是以字符串转换成整数的问题为例,我们写出的代码应该确保能够正确转换最大的正整数和最小的负整数。
  • 错误输入(负面)测试:我们写出的函数除了要顺利地完成要求的功能,当输入不符合要求的时候还能做出合理的错误处理。在设计把字符串转换成整数的函数的时候,我们就要考虑当输入的字符串不是一个数字时,比如“1a2b3c",该怎么告诉函数的调用者这个输入是非法的。

代码的鲁棒性

所谓鲁棒性(Robust),所谓的鲁棒性是指程序能够判断输入是否合乎规范要求,并对不符合要求的输入予以合理的处理。

常用特殊输入

  • int32:正数、负数、0、-2**31、(2**31)-1
  • list:[]
  • ListNode:None、头节点、尾节点 、空链表、单节点链表
  • Tree:功能测试(普通的二叉树;二叉树的所有节点都没有左子树或者右子树只有一个节点的二叉树)。 特殊输入测试(二叉树的根节点为None,也即是一个空树所有节点的值都相同的二叉树)。
  • str:功能测试(输入的字符串中有一个或者多个字符;只有一个字符的字符串;所有字符都唯一的字符串;所有字符都相同的字符串)。 特殊输入测试(输入的字符串的内容为空或者 nullpt r指针)。
  • matrx:功能测试(多行多列的矩阵;一行或者一列的矩阵;只有一个数字的矩阵)。 特殊输入测试(指向矩阵数组的指针为nullptr)。

Python UnitTest Module

unittest的设计灵感最初来源于 Junit 以及其他语言中具有共同特征的单元框架。它支持自动化测试,在测试中使用setup(初始化)和shutdown(关闭销毁)操作,组织测试用例为套件(批量运行),以及把测试和报告独立开来。

通过以下几个示例,你就能理解如何使用unittest了。

示例一:理解 assertEqualtest_isuppertest_split 的区别,以及简单启动测试的方法 unittest.main(),如何测试自定义函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import unittest

def add(x, y):
"""自定义函数
"""
return x + y

class TestStringMethods(unittest.TestCase):
# 参考样例
# 使用 assertEqual 检查是否是预期值
def test_upper(self):
self.assertEqual(add(1, 1), 2) # 这里测试了函数 add

# 使用 assertTrue 或 assertFalse 验证是否符合条件
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())

# 使用 assertRaises 验证是否抛出一个特定异常
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world']) # 这里测试了内建函数str.split()
with self.assertRaises(TypeError):
s.split(2)

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

输出

1
2
3
4
----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK

示例二:理解自主控制测试用例的方法 runner.run(suite) 以及setUp 方法、tearDown 方法的含义,以及如何在一个测试样例中使用多组数据,如何测试类对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import unittest

class compute():

def add(self, x, y):
return x + y

def multipy(self, x, y):
return x * y

def divided(self, x, y):
return x // y


class TestCompute(unittest.TestCase):

@classmethod
def setUpClass(cls):
"""类对象方法 这是所有case的前置条件
"""
cls.test_class = compute() # 所有case共用一个compute实例,到底写在哪里,视实际情况而定。

def setUp(self):
"""示例对象方法 这是每条case的前置条件
"""
pass

def test_add(self):
# 在一个测试样例中使用多组数据
inputs = [(1, 5), (2, 4), (3, 3)]
answer = [6, 6, 6]
for i in range(len(inputs)):
self.assertEqual(self.test_class.add(inputs[i][0], inputs[i][1]), answer[i])

def test_multipy(self):
self.assertEqual(self.test_class.multipy(6, 6), 36)

def test_divided(self):
self.assertEqual(self.test_class.divided(72, 2), 36)

@classmethod
def tearDownClass(cls):
"""这是所有case的后置条件
"""
del cls.test_class # 删除所有case共用的那个compute实例,这里与setUpClass对应。

def tearDown(self):
"""这是每条case的后置条件
"""
pass

if __name__ == '__main__':

# 1、构造用例集
suite = unittest.TestSuite()

# 2、允许添加多个测试,执行顺序是按加载顺序进行。
suite.addTest(TestCompute("test_add"))
suite.addTest(TestCompute("test_multipy"))
# 相较于 unittest.main(),自行构造用例集,可以决定测试哪些用例,不测试哪些用例。
# suite.addTest(TestCompute("test_divided"))

# 3、实例化runner类
runner = unittest.TextTestRunner()

# 4、执行测试
runner.run(suite)

输出

1
2
3
4
----------------------------------------------------------------------
Ran 2 tests in 0.024s

OK

即使,TestCompute有三个用例,但是由于自行构造用例集的过程中只添加了两个所以,也没有像 unittest.main() 那样,全部跑完三个样例。

示例三:一个LeetCode答案及其测试样例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import unittest

class Solution:
def replaceSpace(self, s: str) -> str:
# 特判输入字符为''的情况
if not len(s): return ''
padding = '%20'
# 1. 从' '到'%20'增加了2位,如果有n个空格,就会增加n*2的位置。
count = 0
for i in s:
if i == ' ':
count += 1
# 2. 将长度为len的字符串,增加到长度为len + 2*n,新增的2*n的位置为' '
p1 = len(s) - 1
s += ' ' * count * 2
# 3. 双指针P1指向字符串末尾,在前。P2指向新增长度末尾,在后。
p2 = len(s) - 1
# 5. 一直往前移动,直到P2赶上P1,即可结束。
s = list(s) # 由于str是不可变对象,所以要先转换成list
while p1 != p2 or (s[p1] == ' ' and s[p2] == ' '): # 排除空格位于字符串最前面、最后面、中间连续位置的情况
# 4. 同时前移P1和P2且将P1位置的字符复制到P2位置
if s[p1] != " ":
s[p2] = s[p1]
p1 -= 1
p2 -= 1
else: # 当碰到P1空格时,P1往前移动一格,P2往前移动3格。在这三格分别填充'%'、'2'、'0'。
p1 -= 1
for i in range(2, -1, -1):
s[p2] = padding[i]
p2 -= 1
return "".join(s)

class TestSulotion(unittest.TestCase):
"""声明Python UnitTest Class用于测试
"""
def setUp(self):
self.test_class = Solution()

def test_replaceSpace(self):
s = ["cad eaeb", "a b", "a b ", " a b", " ", "We are happy.", ""]
answer = ["cad%20eaeb", "a%20%20%20b", "a%20b%20%20", "%20%20a%20b", "%20%20%20%20%20", "We%20are%20happy.", ""]
for i in range(len(s)):
self.assertEqual(self.test_class.replaceSpace(s[i]), answer[i])

def tearDown(self):
del self.test_class


if __name__ == "__main__":
# 简单测试的方法
unittest.main()

输出

1
2
3
4
5
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
PS C:\Users\Tommy\.leetcode>