软件测试即是一种实际输出与预期输出间的审核或者比较过程
什么是单元测试
-
什么是单元
代码逻辑中的最小可复用部分:类、函数等
-
单元测试的目标
详细验证所有(一般以代码覆盖率为指标)实现代码的执行逻辑,尤其是各种边界条件
为什么要写单元测试
程序单元作为应用的最小可测试部件。测试能够保证程序的强壮性,也能促使重新思考代码的设计,避免写出耦合严重的代码。
简单来说,就是能够确保代码质量,能够帮助我们较早地发现 Bug;能够改善代码设计,难以测试的代码一般是设计不够简洁的代码;保证重构不会引入新问题,每次修改之前代码的时候,只需重新跑测试代码就能避免新改动的代码对现有代码产生影响。
何时进行单元测试
- 扩展实现代码时(实现新功能时)
- 修改现有代码时(代码重构)
- 任何代码提交时(CI)
如何保证单元测试的隔离性
-
单元测试的隔离性问题
一般我们写的代码,常基于别人的 API 代码构建,同时,我们的代码也常作为别人的 API 代码被调用。在进行单元测试时,实际外部依赖可能包含复杂计算或者存在网络请求,成本较高,不适合重复执行。例如,我们代码依赖的 API 并没有被实现和被提供,比如我们的代码作为C/S模式中的客户端依赖服务端的某个 HTTP 服务;或者,我们的代码运行时可能会对周围代码产/生负面影响,比如发送一个修改数据库数据、发送电子邮件,显然不适合每次执行都真实地进行这些数据修改或者网络请求。
-
如何保证单元测试的隔离性
- 使用 Mock/Stub/Fake 替换外部依赖
- 指定替换逻辑的行为
- 验证带测试逻辑的行为
Python 单元测试相关包
- unittest: Python 内置库,优点是简洁易用,缺点是使用时比较繁琐
- pytest:上手容易,使用起来简洁高效
- mock: 替换掉网络调用或者 rpc 请求等
例子
下面是一个关于几何圆的代码单元测试例子,以说明单元测试在修改现有代码时的作用。
-
圆的实现代码 (circle.py)
# -*- coding: utf-8 -*- # @File : circle.py from math import pi # pi = 3.14 # 后面会修改代码所依赖的 PI 的值,并进行单元测试 class Circle(object): """docstring for Circle""" def __init__(self, radius): super(Circle, self).__init__() self.radius = radius @property def diameter(self): """ 圆的直径 :return: 直径 """ return 2 * self.radius @diameter.setter def diameter(self, value): """ 设置直径 :param value: 直径的值 :return: None """ self.radius = value / 2.0 @diameter.deleter def diameter(self): raise AttributeError("Can't delete attribute") @property def perimeter(self): """ 圆的周长 :return: 周长 """ return 2 * pi * self.radius @perimeter.setter def perimeter(self, value): self.radius = value / pi / 2.0 @perimeter.deleter def perimeter(self): raise AttributeError("Can't delete attribute") @property def area(self): """ 圆的面积 :return: 面积 """ return pi * (self.radius ** 2) @area.setter def area(self, value): self.radius = (value / pi) ** (1 / 2) @area.deleter def area(self): raise AttributeError("Can't delete attribute") def print_circle_info(c1): print("radius: {}".format(c1.radius)) print("diameter: {}".format(c1.diameter)) print("perimeter: {}".format(c1.perimeter)) print("area: {}".format(c1.area)) if __name__ == '__main__': c1 = Circle(1) print_circle_info(c1) print("\n================") print("set diameter to 4") print("================") c1.diameter = 4 print_circle_info(c1) print("\n================") print("set perimeter to 6.28") print("================") c1.perimeter = 6.28 print_circle_info(c1)
-
单元测试代码 ( test_circle_pytest.py )
# -*- coding: utf-8 -*- # @File : test_circle_pytest.py import pytest from circle import Circle class TestCirclePytest: def test_init(self): c0 = Circle(1) assert c0.radius == 1 def test_diameter(self): c0 = Circle(2) assert c0.diameter == 4 c0.diameter = 8 assert c0.diameter == 8 with pytest.raises(AttributeError): del c0.diameter def test_perimeter(self): c0 = Circle(1) assert c0.perimeter - 6.28318 == pytest.approx(0, abs=1E-5) c0.perimeter = 6.28 assert c0.perimeter == 6.28 assert abs(c0.radius - 1.0) == pytest.approx(0, abs=0.01) with pytest.raises(AttributeError): del c0.perimeter def test_area(self): c0 = Circle(1) assert c0.area - 3.14159 == pytest.approx(0, abs=1E-5) c0.area = 3.14 assert c0.area == 3.14 with pytest.raises(AttributeError): del c0.area
-
代码实现后的单元测试结果
代码实现后,运行单元测试代码,显然,结果显示代码实现基本没问题
-
修改代码中的某些部分
实际写代码的时候,总会因为某些需求,会需要修改现已实现的代码。为了模拟,这种需求,这里我们修改代码所依赖的 PI 值。修改
circle.py
中的 PI 值为 3.14 :# -*- coding: utf-8 -*- # @File : circle.py # from math import pi # 去掉之前的 PI 值的定义 pi = 3.14 其他代码省略 ...
-
修改代码后重新运行单元测试代码
从运行结果可以看到,修改 pi 的值后,单元测试中部分测试样例没有通过,分析可以发现,主要是因为 pi 值的精度达不到我们测试代码的要求。将 PI 值修改为 3.141592526 显然就可以,甚至可以一点点增加 PI 的精度,直到通过单元测试。
参考链接
-
- 单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。
- 单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
- 单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。
- 单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。