Python代码的单元测试
单元测试的概念
-
定义:是指对软件中的最小可测试单元进行检查和验证。
-
作用:可以确保程序模块是否否和我们规范的输出,保证该模块经过修改后仍然是满足我们的需求。
单元测试的策略
如果要创建单元测试,可以遵循如下基本技巧来确保涵盖所有的测试用例。
-
逻辑检查:给定正确的、符合预期的输入,系统是否能够执行正确的计算并遵循通过代码正确的路径?给定的输入是否涵盖通过代码的所有路径?
-
边界检查:对于给定的输入,系统如何响应? 系统如何响应典型输入、边缘用例或无效输入?假设您期望输入的整数介于 3 和 7 之间。当您使用 5(典型输入)、3(边缘用例)或 9(无效输入)时,系统会如何响应?
-
错误处理:当输入中出现错误时,系统会如何响应? 是否提示用户输入其他内容? 软件是否会崩溃?
-
面向对象的检查:如果通过运行代码更改任何持久对象的状态,则该对象是否正确更新?
单元测试类的编写
-
首先编写要进行单元测试的代码。
def add(x, y):"""加法函数"""return x + ydef subtract(x, y):"""减法函数"""return x - ydef multiply(x, y):"""乘法函数"""return x * ydef divide(x, y):"""除法函数"""if y != 0:return x / yelse:return ValueError("除数不能为0")
-
编写测试工具代码。
# 测试代码应使用test_xxx的名称进行规范 # 这里使用断言assert进行模拟 # assert(断言)用于判断一个表达式,在表达式条件为 false 的时候触发异常 # 如下代码执行正常不报错,则说明函数执行符合预期 from myfunctions import divide# 一个简单的单元测试示例 def test_divide(x, y, result):r = divide(x, y)assert result == rdef test_divide_error(x, y):try:divide(x, y)except ValueError:assert False
-
执行单元测试的工具代码。
单元测试执行完成,无错误输出代码执行符合预期。
Python中的单元测试类
如上的代码能够满足Python中进行单元测试的需求,但如果换一套方法,我们的单元测试又需要手动编码,Python中提供了专业的单元测试工具类:unittest
,以下是该单元测试类的使用方法介绍。
unittest
将我们常规用到的测试场景封装了以下断言方法,根据测试所需要的场景进行引用。
断言方法 | 方法解释 |
---|---|
assertEqual(a, b) | 检查a 和b 是否相等。 |
assertNotEqual(a, b) | 检查a 和b 是否不相等。 |
assertTrue(x) | 检查x 是否为True。 |
assertFalse(x) | 检查x 是否为False。 |
assertIs(a, b) | 检查a 和b 是否为同一对象(is)。 |
assertIsNot(a, b) | 检查a 和b 是否不是同一对象。 |
assertIsNone(x) | 检查x 是否为None。 |
assertIsNotNone(x) | 检查x 是否不是None。 |
assertIn(a, b) | 检查a 是否在b 中。 |
assertNotIn(a, b) | 检查a 是否不在b 中。 |
assertIsInstance(a, b) | 检查a 是否为b 类型的实例。 |
assertNotIsInstance(a, b) | 检查a 是否不是b 类型的实例。 |
assertAlmostEqual(a, b) | 检查a 和b 是否近似相等(适用于浮点数比较)。 |
assertNotAlmostEqual(a, b) | 检查a 和b 是否不近似相等(适用于浮点数比较)。 |
assertRaises(Error, func, *args, **kwargs) | 检查当调用function 时是否抛出了Error 异常。 |
unittest模块的基础使用
单元测试代码:
# unittest 是python自带的工具包,无需单独下载
import unittest
from myfunctions import *class TestMyFunctions(unittest.TestCase):def test_add(self):# assertEqual,断言测试方法,`unittest` 将我们常规用到的测试场景封装成断言方法,根据测试所需要的场景进行引用。self.assertEqual(add(1, 2), 3)self.assertEqual(add(-1, 2), 1)self.assertEqual(add(-1, -2), -3)def test_subtract(self):self.assertEqual(subtract(1, 2), -1)self.assertEqual(subtract(-1, 2), -3)self.assertEqual(subtract(-1, -2), 1)def test_multiply(self):self.assertEqual(multiply(1, 2), 2)self.assertEqual(multiply(-1, 2), -2)self.assertEqual(multiply(-1, -2), 2)def test_divide(self):self.assertEqual(divide(1, 2), 0.5)self.assertEqual(divide(-1, 2), -0.5)self.assertEqual(divide(-1, -2), 0.5)
unittest
代码并不能够直接运行,有以下执行方法:
-
命令行:
python -m unittest test_myfunctions.py
注意:这里的启动如果写成
python.exe -m unittest .\test_myfunctions.py
会报如下错误。 -
main
函数:if __name__ == '__main__':unittest.main()
unittest模块的前置方法
在实际的测试中可能同时存在多个前置相同的测试,unittest
模块提供了setUp()
用于在测试开始前执行相关环境的设置,tearDown()
在 setUp()
方法之后进行执行。
注:这里的 setUp
和 tearDown
是固定的函数名,不允许更改。
要测试的代码:
def divide(x, y):"""除法函数"""print("divide called")if y != 0:return x / yelse:raise ValueError("除数不能为0")
测试工具类:
import unittest
from myfunctions import *class TestMyFunctions(unittest.TestCase):def setUp(self):self.test_value_a = 10self.test_value_b = 5self.test_value_c = 0print("setUp called")def tearDown(self):del self.test_value_adel self.test_value_bdel self.test_value_cprint("tearDown called")def test_divide(self):self.assertEqual(divide(self.test_value_a, self.test_value_b), 2)self.assertRaises(ValueError, divide, self.test_value_a, self.test_value_c)if __name__ == '__main__':unittest.main()
代码执行测试:
可以看到,在执行函数测试之前,首先调用了 setUp()
前置方法,然后执行测试,测试结束后,调用 tearDown()
清理单元测试。
测试覆盖率
覆盖率是用来度量测试完整性的手段,是测试效果衡量的标准,是测试技术有效性的度量: 覆盖率 = (至少被执行一次的项目(item)数) / (项目的总数)
Python中提供了测试覆盖率的模块类:coverage,该第三方包需要手动安装。
这里使用如下方法直接进行代码的测试覆盖率分析
# 调用单元测试工具类
coverage run -m unittest discover
# 输出测试报告
coverage report
这里的代码测试覆盖率较低,对单元测试工具类进行优化,要测试的代码如下:
def add(x, y):"""加法函数"""return x + ydef subtract(x, y):"""减法函数"""return x - ydef multiply(x, y):"""乘法函数"""return x * ydef divide(x, y):"""除法函数"""print("divide called")if y != 0:return x / yelse:raise ValueError("除数不能为0")
优化后的单元测试方法为:
import unittest
from myfunctions import *class TestMyFunctions(unittest.TestCase):def setUp(self):self.test_value_a = 10self.test_value_b = 5self.test_value_c = 0print("setUp called")def tearDown(self):del self.test_value_adel self.test_value_bdel self.test_value_cprint("tearDown called")def test_add(self):self.assertEqual(add(self.test_value_a, self.test_value_b), 15)self.assertEqual(add(self.test_value_a, self.test_value_c), 10)def test_subtract(self):self.assertEqual(subtract(self.test_value_a, self.test_value_b), 5)self.assertEqual(subtract(self.test_value_a, self.test_value_c), 10)def test_multiply(self):self.assertEqual(multiply(self.test_value_a, self.test_value_b), 50)self.assertEqual(multiply(self.test_value_a, self.test_value_c), 0)def test_divide(self):self.assertEqual(divide(self.test_value_a, self.test_value_b), 2)self.assertRaises(ValueError, divide, self.test_value_a, self.test_value_c)if __name__ == '__main__':unittest.main()
结果测试