1. 单元测试

1.1 什么是单元测试?

单元测试是一种软件测试方法,主要用于验证软件的最小可测试单元(通常是函数、方法或类)是否按预期工作。

最小可测单元:函数、方法、类、文件、模块

1.2 单元测试落地挑战

  1. 不知道怎么编写单元测试
    • 结对测试:由测试设计提供单元测试用例,由开发编写实现单元测试代码
  2. 公司、团队、项目没有要求
    • 寻找工作痛点、收集数据、分析问题原因、提出解决方案
  3. 单元测试价值不高,写了也是浪费时间

1.3 单元测试怎么做?

首先,需要结合你的团队成员的技能、意愿、当前问题、项目迭代周期等等。

任何体能增销售端一定是要建立在合适的时机来切入,不能一味的强推!

  1. 试点(找一两个团队试试水,取得效果后再全面推广)
  2. 要有优先级,从 P0 核心模板开始
  3. 要有度量,需要衡量你做这件事的价值

2. Unittest 基础

2.1 框架介绍

官方文档:https://docs.python.org/zh-cn/3/library/unittest.html

Unittest 是 Python 标准库中自带的单元测试框架,Unittest 也被称为 PyUnit,就像 JUnit 是 Java 语言的标准单元测试框架一样,Unittest 则是 Python 语言的标准单元测试框架。

Unittest 单元测试框架不仅可以适用于 Python 项目的单元测试,还可以适用接口测试、Web 自动化测试等,它有一个很重要的特性:它是通过类(class)的方式,将测试用例组织在一起。

2.2 核心要素及常用属性

核心要素

Unittest 框架中最核心的四个概念是:test case、testsuite、test runner 和 test fixture。

  • 一个 TestCase 的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
  • 而多个测试用例集合在一起,就是 TestSuite,而且 TestSuite 也可以嵌套 TestSuite。
  • TestLoader 是用来加载 TestCase 到 TestSuite 中的,其中有几个 loadTestsFrom__() 方法,就是从各个地方寻找 TestCase,创建它们的实例,然后 add 到 TestSuite 中,再返回一个 TestSuite 实例。
  • TextTestRunner 是来执行测试用例的,其中的 run(test) 会执行 TestSuite / TestCase 中的 run(result) 方法。
  • 测试的结果会保存到 TextTestResult 实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。
  • 而对一个测试用例环境的搭建和销毁,是一个 fixture。

这样整个流程就清楚了,首先是要写好 TestCase,然后由 TestLoader 加载 TestCase 到 TestSuite,然后由 TextTestRunner 来运行 TestSuite,运行的结果保存在 TextTestResult 中,整个过程集成在 unittest.main 模块中。

常用属性

uniitest 的属性如下:

说明:

  • unittest.TestCase:TestCase 类,所有测试用例类继承的基类。
  • unittest.TestSuite():unittest 框架的 TestSuite() 类是用来创建测试套件的。
  • unittest.main():将一个单元测试模块变为可直接运行的测试脚本,main() 方法使用 TestLoader 类来搜索所有包含在该模块中以 “test” 命名开头的测试方法,并自动执行他们,根据 ASCII 码的顺序加载测试用例,数字与字母的顺序为:0-9,A-Z,a-z。
  • unittest.TextTestRunner():unittest 框架的 TextTestRunner() 类,通过该类下面的 run() 方法来运行 suite 所组装的测试用例,入参为 suite 测试套件。

2.3 基本使用

我们用一个基础的计算器的例子来看看 unittest 是如何使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 计算器类
class Calculator(object):
def __init__(self, a, b):
self.a = a
self.b= b

def add(self):
return self.a + self.b

def minus(self):
return self.a - self.b

def multip(self):
return self.a * self.b

def divide(self):
return self.a / self.b

我们需要单独创建一个写单元测试的文件出来,并且编写步骤如下:

  • 导入 unittest 模块、被测文件或者其中的类
  • 创建一个测试类,并继承 unittest.TestCase
  • 重写 setUp 和 tearDown 方法(可选,如果有初始化条件和结束条件)
  • 定义测试函数,函数以 test 开头
  • 在函数体中使用断言来判断测试结果是否符合预期结果
  • 调用 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# !/usr/bin/python3
# -*- coding: utf-8 -*-

# 导入 unittest 模块
import unittest
from calculator import Calculator

# 定义测试类,父类为 unittest.TestCase
class TestCalculator(unittest.TestCase):
# 定义 setUp() 方法用于测试用例执行前的初始化工作
def setUp(self):
print('test start')
self.result = Calculator(20, 5)

# 定义测试用例,以 "test_" 开头命名的方法,方法的入参为 self
def test_add(self):
"""Test method add(a, b)"""
print('测试加法函数')
self.assertEqual(self.result.add(), 25, '计算错误!')

def test_minus(self):
"""Test method minus(a, b)"""
print('测试减法函数')
self.assertEqual(self.result.minus(), 15, '计算错误!')

def test_multip(self):
"""Test method multip(a, b)"""
print('测试乘法函数')
self.assertEqual(self.result.multip(), 100, '计算错误!')

def test_divide(self):
"""Test method divide(a, b)"""
print('测试除法函数')
self.assertEqual(self.result.divide(), 4, '计算错误!')

# 定义 tearDown() 方法用于测试用例执行之后的善后工作
def tearDown(self):
print('test end')

if __name__ == '__main__':
# unittest.main() 方法会搜索该模块下所有以 test 开头的测试用例方法,并自动执行它们
unittest.main()

这里执行时需要注意的一个点是,执行测试用例的顺序不是按照文件中定义的顺序,而是根据 ASCII 码的顺序加载测试用例,数字与字母的顺序为:0-9,A-Z,a-z。

如果想按照指定的顺序来执行用例,则可以在用例名称中增加一个数字的标识

1
2
3
4
5
6
7
def test_001_add(self):...

def test_002_minus(self):...

def test_003_multip(self):...

def test_004_divide(self):...

3. Unittest 常见用法

3.1 TestSuite 测试套件

3.1.1 加载单个用例

除了上面基本使用里的方法可以执行测试用例,我们同样也可以使用 TestSuite 测试套件来执行测试用例。

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
# !/usr/bin/python3
# -*- coding: utf-8 -*-

# 导入 unittest 模块
import unittest
from calculator import Calculator

# 定义测试类,父类为 unittest.TestCase
class TestCalculator(unittest.TestCase):
# 定义 setUp() 方法用于测试用例执行前的初始化工作
def setUp(self):
print('test start')
self.result = Calculator(20, 5)

# 定义测试用例,以 "test_" 开头命名的方法,方法的入参为 self
def test_add(self):
"""Test method add(a, b)"""
print('测试加法函数')
self.assertEqual(self.result.add(), 25, '计算错误!')

def test_minus(self):
"""Test method minus(a, b)"""
print('测试减法函数')
self.assertEqual(self.result.minus(), 15, '计算错误!')

def test_multip(self):
"""Test method multip(a, b)"""
print('测试乘法函数')
self.assertEqual(self.result.multip(), 100, '计算错误!')

def test_divide(self):
"""Test method divide(a, b)"""
print('测试除法函数')
self.assertEqual(self.result.divide(), 4, '计算错误!')

# 定义 tearDown() 方法用于测试用例执行之后的善后工作
def tearDown(self):
print('test end')

if __name__ == '__main__':
# 1.定义测试套件,构造测试机
suite = unittest.TestSuite()

# 2.向测试套件中添加测试用例,添加顺序即为用例执行顺序
# 写法一:单个添加测试用例
suite.addTest(TestCalculator('test_divide'))
suite.addTest(TestCalculator('test_add'))
suite.addTest(TestCalculator('test_minus'))
suite.addTest(TestCalculator('test_multip'))
# 写法二:批量添加测试用例
# tests = [TestCalculator('test_add'), TestCalculator('test_minus'), TestCalculator('test_multip')]
# suite.addTests(tests)

# 3.运行容器中的测试用例
runner = unittest.TextTestRunner()
runner.run(suite)

3.1.2 加载多个类

上面这种方式添加测试用例会比较麻烦,当用例很多的时候显然一个个添加就不太合适了。

因此可以通过 unittest.TestLoader().loadTestsFromTestCase(类名) 可以一次加载某个类下面的所有测试用例,也支持加载多个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import unittest
import test_myclass1 import TestCalculator1
import test_myclass2 import TestCalculator2

if __name__ == '__main__':
# 根据给定的测试类,获取其中所有以 "test" 开头的测试方法,并返回一个测试套件
loader = unittest.TestLoader
suite1 = loader.loadTestsFromTestCase(TestCalculator1)
suite2 = loader.loadTestsFromTestCase(TestCalculator2)

# 将多个测试类加载到测试套件中
# 通过调整 suit2 和 suite1 的顺序,可以设定执行顺序
suite = unittest.TestSuite([suite2, suite1])

runner = unittest.TextTestRunner()
runner.run(suite)

3.1.3 匹配目录下的用例

假如某个目录下存在多个测试模块,test1.py 和 test2.py、test3.py,如果用 addTests 的方法添加用例到测试套件,未免有点麻烦,这时可以用到 TestLoader() 这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os
import unittest


if __name__ == '__main__':
# 1. 实例化测试套件对象
suite = unittest.TestSuite()
# 2. 实例化 TestLoader 对象
loader = unittest.TestLoader()
# 3. 使用 addTests 将找到的测试用例放在测试套件下
suite.addTests(loader.discover(os.getcwd()))

# 或者另外一种写法
# suite = unittest.defaultTestLoader.discover('./test', pattern='test_*.py')

# 运行
runner = unittest.TextTestRunner()
runner.run(suite)

3.2 保存用例执行结果

如果想将用例执行的结果持久化的保存下来,则可以通过下面的方法将结果写入到一个文件中。

1
2
3
4
5
6
7
8
9
10
11
if __name__ == '__main__':
suite = unittest.TestSuite()

suite.addTest(TestCalculator('test_divide'))
suite.addTest(TestCalculator('test_add'))
suite.addTest(TestCalculator('test_minus'))
suite.addTest(TestCalculator('test_multip'))

with open('UnittestTextReport.txt', 'a') as f:
runner = unittest.TextTestRunner(stream=f, verbosity=2)
runner.run(suite)

Tips:

如果想要执行的结果更加详细,则可以指定 verbosity 属性为 2。

3.3 setUp() 和 tearDown()

setUp() 和 tearDown() 这两个方法在每个测试方法执行前以及执行后执行一次,setUp 用来为测试准备环境,tearDown 用来清理环境。

如果想要在一个类的所有 case 执行之前准备一次环境,并在所有 case 执行纪结束之后再清理环境,我们可以用 setUpClass() 与 tearDownClass()。

3.4 跳过某个 Case

如果我们临时想要跳过某个 case 不执行怎么办?unittest 也提供了两种方法:

  • skip 装饰器
    • @unittest.skip(reason):skip(reason) 装饰器无条件跳过装饰的测试,并说明跳过测试的原因。
    • @unittest.skiplf(reason):skiplf(condition, reason) 装饰器条件为真时,跳过装饰的测试,并说明跳过测试的原因。
    • @unittest.skipUnless(reason):skipUnless(condition, reason) 装饰器条件为假时,跳过装饰的测试,并说明跳过测试的原因。
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
# !/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
def setUp(self):
print('test start')
self.result = Calculator(20, 5)

@unittest.skip("I don't want to run this case.")
def test_add(self):
"""Test method add(a, b)"""
print('测试加法函数')
self.assertEqual(self.result.add(), 25, '计算错误!')

@unittest.skipIf(3 > 2, "3 大于 2,此用例不执行")
def test_minus(self):
"""Test method minus(a, b)"""
print('测试减法函数')
self.assertEqual(self.result.minus(), 15, '计算错误!')

def test_multip(self):
"""Test method multip(a, b)"""
print('测试乘法函数')
self.assertEqual(self.result.multip(), 100, '计算错误!')

# 除非是 Linux 平台,否则忽略此方法
@unittest.skipUnless(sys.platform.startswith("linux"), "requires Linux")
def test_divide(self):
"""Test method divide(a, b)"""
print('测试除法函数')
self.assertEqual(self.result.divide(), 4, '计算错误!')

def tearDown(self):
print('test end')

if __name__ == '__main__':
suite = unittest.TestSuite()

suite.addTest(TestCalculator('test_divide'))
suite.addTest(TestCalculator('test_add'))
suite.addTest(TestCalculator('test_minus'))
suite.addTest(TestCalculator('test_multip'))

runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)

  • TestCase.skipTest() 方法
1
2
3
4
5
def test_multip(self):
"""Test method multip(a, b)"""
self.skipTest('Do not run this.')
print('测试乘法函数')
self.assertEqual(self.result.multip(), 100, '计算错误!')

3.5 断言方法

一般接口测试断言需要关注的地方:

  1. HTTP 状态码的验证:查看状态返回是否是 200,即 HTTP 连接是成功的
  2. 业务状态码的验证:当前业务下该接口调用返回的值是否是正常的,例如返回 status = 0 表示正常
  3. 数据的验证:对返回的数据进行断言,验证数据是否符合预期
  4. 性能的验证:一个接口的响应时间不能过长
断言 作用
assertEqual(a, b, [msg='失败时打印的信息') 断言 a,b 是否相等,相等则用例通过。
assertNotEqual(a, b, [msg='失败时打印的信息') 断言 a,b 是否相等, 不相等则试用例通过
assertTrue(x, [msg='失败时打印的信息']) 断言 x 是否 True,是 True 则用例通过。
assertFalse(x, [msg='失败时打印的信息') 断言 x 是否 False,是 False 则用例通过。
assertIs(a, b, [msg='失败时打印的信息']) 断言 a 是否是 b,是则用例通过。
assertNotIs(a, b, [msg='失败时打印的信息') 断言 a 是否是 b,不是则用例通过。
assertlsNone(x, [msg='失败时打印的信息']) 断言 x 是否 None,是 None,是 None 则用例通过。
assertIsNotNone(x, [msg='失败时打印的信息]) 断言 x 是否 None,不是 None 则用例通过。
assertln(a, b, [msg='失败时打印的信息') 断言 a 是否在 b 中,在 b 中则用例通过。。
assertNotIn(a, b, [msg='失败时打印的信息']) 断言 a 是否在 b 中,不在 b 中则用例通过
assertIsInstance(a, b, [msg='失败时打印的信息') 断言 a 是 b 的一个实例,是则用例通过。
assertNotIsInstance(a, b, [msg='失败打印的信息']) 断言 a 不是 b 的一个实例,不是则用例过。

3.6 命令行执行用例方式

unittest 支持执行指定用例:

  • 指定测试模块
1
python -m unittest test_module1 test_module2
  • 指定测试类
1
python -m unittest test_module.TestClass
  • 指定测试方法
1
python -m unittest test_module.TestClass.test_method

unittest 支持用例自动(递归)发现:

  • 默认发现当前目录下所有符合 test*.py 测试用例

    使用 python -m unittest 或 python -m unittest discover

  • 通过 -s 参数指定要自动发现的目录,-p 参数指定用例文件的名称模式

1
python -m unittest discover -s project_directory -p "test_*.py"