单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
如果发现代码难以构造测试,很有可能就是接口设计不够优雅,或者耦合严重,尝试从测试的角度思考能够让我们更好地设计。单元测试同时也为重构提供了保证,比如我们想优化一个函数内部实现,更换更优的数据结构和算法,只需要重新跑一下测试就可以验证新的实现是否引入了错误或bug。
单元测试有以下好处:
- 确保代码质量
- 改善代码设计,难以测试的代码一般是设计不够简洁的代码。
- 保证重构不会引入新问题,以函数为单位进行重构的时候,只需要重新跑测试就基本可以保证重构没引入新问题。
单元测试的框架
关于单元测试,有很多现成的框架,其特点如下:
- unittest,内置库,模仿PyUnit写的,简洁易用,缺点是比较繁琐。
- nose,测试发现,发现并运行测试。
- pytest,写起来很方便,并且很多知名开源项目在用,推荐。
- mock,替换掉网络调用或者 rpc 请求等
由于pytest使用起来比较方便,因此选择pytest作为python代码单元测试的工具,关于pytest的具体介绍和使用方法。
安装
1 | sudo pip install -U pytest |
嵌入代码内的测试
此处参考官网示例,新建一个测试文件。1
2
3
4
5
6# test_sample.py的内容
def func(x):
return x + 1
def test_answer():
assert func(3) == 5
直接在脚本目录中运行 pytest
,输出如下
1 | PS D:\Git_Repo\Work_temp> pytest.exe |
pytest会在当前的目录下,寻找以 test 开头的文件(即测试文件),
找到测试文件之后,进入到测试文件中寻找test_开头的测试函数并执行。
通过上面的测试输出,我们可以看到该测试过程中,一个收集到了一个测试函数,测试结果是失败的(标记为F),并且在FAILURES部分输出了详细的错误信息,帮助我们分析测试原因,我们可以看到”assert func(3) == 5”这条语句出错了,错误的原因是func(3)=4,然后我们断言func(3) 等于 5。
拆分的测试代码
我们可以看到嵌入代码内的测试有一个很大的弊端,就是严格限制了代码文件的名称,一方面非常容易引起歧义,另一方面测试代码再实际业务层面其实是属于冗余代码的范畴,代码杂糅不利于后期的管理。
所以大部分业务逻辑下,我们其实是需要将测试代码和业务代码分离的。这时候,我们需要把我们的业务代码进行打包(代码目录创建 __init__.py
)
示例如下,我们再 get_max
目录下创建 get_max/get_max.py
和get_max/__init__.py
get_max/get_max.py
1
2
3
4
5def max(a,b):
if a > b:
return a
else:
return bget_max/init.py
1
from .get_max import max
然后创建我们的单元测试代码test_temp.py
1 | import pytest |
然后运行 pytest
结果如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21PS D:\Git_Repo\Work_temp> pytest.exe
============================= test session starts ==============================
platform win32 -- Python 3.11.8, pytest-8.0.2, pluggy-1.3.0
rootdir: D:\Git_Repo\Work_temp
plugins: anyio-3.7.1
collected 2 items
test_temp.py .F [100%]
=================================== FAILURES ===================================
__________________________________ test_max1 ___________________________________
def test_max1():
> assert max(1,2)==3
E assert 2 == 3
E + where 2 = max(1, 2)
test_temp.py:8: AssertionError
=========================== short test summary info ============================
FAILED test_temp.py::test_max1 - assert 2 == 3
========================= 1 failed, 1 passed in 0.55s ==========================
可以看到我们的两个测试用例,一个pass,一个fail,其中失败的是 test_max1。
补充说明
pytest测试样例非常简单,只需要按照下面的规则:
- 测试文件以test_开头(以_test结尾也可以)
- 测试类以Test开头,并且不能带有 init 方法
- 测试函数以test_开头
- 断言使用基本的assert即可
执行测试样例的方法很多种,上面第一个实例是直接执行pytest,第二个实例是传递了测试文件给pytest。其实pytest有好多种方法执行测试:1
2
3
4
5
6
7
8
9
10pytest # run all tests below current dir
pytest test_mod.py # run tests in module
pytest somepath # run all tests below somepath
pytest -k stringexpr # only run tests with names that match the
# the "string expression", e.g. "MyClass and not method"
# will select TestMyClass.test_something
# but not TestMyClass.test_method_simple
pytest test_mod.py::test_func # only run tests that match the "node ID",
# e.g "test_mod.py::test_func" will select
# only test_func in test_mod.py