1. What: 单测框架
1.1 什么是单测
单元测试是自动化测试的一种形式--这仅仅意味着测试计划是由一个脚本执行的,而不是由人手动执行的。
它们作为软件测试的第一层,通常以函数的形式编写,验证软件程序中各种功能的行为。
1.2 为什么要写单元测试
以下列举了一些我为什么使用单元测试的好处:
- 减少bug:允许你对代码做出任何改变,因为你了解单元测试会在你的预期之中。单元测试可以有效地降低程序出现BUG的机率。
- 重构:帮助你更深入地理解代码--因为在写单元测试的时候,你需要明确程序所有的执行流程及对应的执行结果等等; 允许在任何时候代码重构,而不必担心破坏现有的代码。这使得我们编写程序更灵活,确保你的代码的健壮性,因为所有的测试都是通过了的。
- 文档记录:单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
- 回归性:自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。
2. Why: 为什么是Pytest
官网上的pytest:
helps you write better programs.
The pytest framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries.
测试是如此重要,以至于Python带有自己的内置测试框架,称为unittest。但是,在unittest中编写测试可能很复杂,因此近年来,pytest框架已成为标准。
- 非常容易開始,因為它的簡單和容易的語法。
- 可以並行執行測試。
- 可以執行特定測試或測試子集
- 自動檢測測試
- 跳過測試
- 開源
- 使用普通的断言语句而不是 unittest 的 assertSomething 方法(例如,assertEquals、assertTrue)
- 简化了测试状态的设置和拆卸
3. How:Pytest教程
3.1 安装
pip install -U pytest
pip install -U pytest-html
pip install -U pytest-rerunfailures
3.2 一个简单的例子
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
执行测试:
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_sample.py F [100%]
================================= FAILURES =================================
_______________________________ test_answer ________________________________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_sample.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_sample.py::test_answer - assert 4 == 5
============================ 1 failed in 0.12s =============================
执行测试的时候,我们只需要在测试文件testsample所在的目录下,运行pytest即可。
pytest会在当前的目录下,寻找以test开头的文件(即测试文件),找到测试文件之后,进入到测试文件中寻找test开头的测试函数并执行。
通过上面的测试输出,我们可以看到该测试过程中,一个收集到了一个测试函数,测试结果是失败的(标记为F),并且在FAILURES部分输出了详细的错误信息,帮助我们分析测试原因。
我们可以看到”assert func(3) == 5”这条语句出错了,错误的原因是func(3)=4,然后我们断言func(3) 等于 5。
3.3 如何编写pytest测试
3.3.1 规则
3.3.1.1 编写规则
pytest执行单测需要按照下面的规则:
- 测试文件以test_开头(以_test结尾也可以)
- 测试类以Test开头,并且不能带有 init 方法
- 测试函数以test_开头
- 断言使用基本的assert即可
3.3.1.2 执行规则
执行测试:
pytest # 在当前目录下执行所有测试用例
pytest test_mod.py # 执行指定文件中的所有测试用例
pytest somepath # 执行指定目录下的所有测试用例
pytest -k stringexpr # 执行匹配到字符串的测试用例
pytest test_mod.py::test_func # 执行指定文件中的指定测试用例
3.3.2 测试函数
# content of test_sample.py
def func(x):
return x+1
def test_func():
assert func(3) == 5
3.3.3 测试类
# content of test_class.py
class TestClass:
def test_one(self):
x = "this"
assert 'h' in x
def test_two(self):
x = "hello"
assert hasattr(x, 'check')
执行测试:
$ pytest -q test_class.py
.F
================================= FAILURES =================================
____________________________ TestClass.test_two ____________________________
self = <test_class.TestClass object at 0x7fbf54cf5668>
def test_two(self):
x = "hello"
> assert hasattr(x, 'check')
E assert hasattr('hello', 'check')
test_class.py:8: AssertionError
1 failed, 1 passed in 0.01 seconds
pytest -q 可以不在输出版本信息。
该测试共执行了两个测试样例,一个失败一个成功。同样,我们也看到失败样例的详细信息,和执行过程中的中间结果。
pytest中用点号(.)表示一条用例被执行并通过,用F来标识一条用例被执行并失败。
如果想查看详情可以在pytest命令后面加上-v或者–verbose选项:
$ pytest -v test_class.py
collected 2 items
test_class.py::TestClass::test_one PASSED [ 50%]
test_class.py::TestClass::test_two FAILED [100%]
======================================================= FAILURES =======================================================
__________________________________________________ TestClass.test_two __________________________________________________
self = <test_class.TestClass object at 0x7fbcb0a15030>
def test_two(self):
x = "hello"
> assert hasattr(x, 'check')
E AssertionError: assert False
E + where False = hasattr('hello', 'check')
test_class.py:10: AssertionError
3.3.4 使用raises在单测中捕获异常
有时候我们在做测试的时候,预期就是抛出一个异常,但如果在正常情况下,抛出异常后pytest或停止该条测试用例的继续执行,所以我们需要一个期望抛出异常的方法,pytest.raises就可以做到这一点,代码如下:
# content of test_sysexit.py
import pytest
def f():
raise SystemExit(1)
def test_mytest():
# 此处必须抛出SystemExit的异常,用例才会通过
with pytest.raises(SystemExit):
f()
def test_add_raises():
with pytest.raises(AssertionError):
# 此处必须抛出AssertionError的异常,用例才会通过
assert 1 + 1 == 3
3.3.5 使用固件(fixture)
固件(Fixture)是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们。
固件是为了最大复用代码,以及在固定流程中注入代码使用。
3.3.5.1 使用
在函数声明之前加上“@pytest.fixture”。其他函数要来调用这个Fixture,只用把它当做一个输入的参数即可。
import pytest
@pytest.fixture()
def before():
print('\nbefore each test')
def test_1(before):
print('test_1()')
def test_2(before):
print('test_2()')
assert 0
除了在函数参数调用外,还可以显示指定调用,下面三种方式都是一样的功能:
import pytest
# 固件定义
@pytest.fixture()
def before():
print('\nbefore each test')
# 函数前指定
@pytest.mark.usefixtures("before")
def test_1():
print('test_1()')
@pytest.mark.usefixtures("before")
def test_2():
print('test_2()')
class Test1:
# 类的成员函数前指定
@pytest.mark.usefixtures("before")
def test_3(self):
print('test_4()')
@pytest.mark.usefixtures("before")
def test_4(self):
print('test_4()')
# 类的定以前指定
@pytest.mark.usefixtures("before")
class Test2:
def test_5(self):
print('test_5()')
def test_6(self):
print('test_6()')
除了上述定在一个指定函数外,还可以文件conftest.py集中管理固件(pytest会自动调用)。
# conftest.py
import pytest
@pytest.fixture()
def data():
return 3
# test_fun.py
# 测试成功
def test_pass(data):
assert func(data) == 4
Pytest 使用 yield 关键词将固件分为两部分,yield 之前的代码属于预处理,会在测试前执行;yield 之后的代码属于后处理,将在测试完成后执行。
# conftest.py
import pytest
@pytest.fixture()
def db():
print('opened')
yield
print('closed')
3.3.5.2 固件的作用范围
fixture的scope参数用来作用范围,scope参数有四种,’function’,’module’,’class’,’session’,默认为function。
- function:每个test都运行,默认是function的scope
- class:每个class的所有test只运行一次
- module:每个module的所有test只运行一次
- session:每个session只运行一次
```pythonconftest.py
import pytest
@pytest.fixture(scope='function', autouse=True)
def func_scope():
print('opened')
yield
print('closed')
@pytest.fixture(scope='module', autouse=True)
def mod_scope():
pass
@pytest.fixture(scope='session')
def sess_scope():
pass
@pytest.fixture(scope='class')
def class_scope():
pass
```python
# test_fun.py
import pytest
@pytest.mark.usefixtures('sess_scope')
def test_pass(class_scope):
assert func(3) == 4
输出结果:
# 使用 --setuo-show 选项,显示详细的固件信息
$ pytest --setup-show -k pass test_fun.py
collected 2 items / 1 deselected / 1 selected
test_sample.py
SETUP S sess_scope
SETUP M mod_scope
SETUP C class_scope
SETUP F func_scope
test_sample.py::test_pass (fixtures used: class_scope, func_scope, mod_scope, sess_scope).
TEARDOWN F func_scope
TEARDOWN C class_scope
TEARDOWN M mod_scope
TEARDOWN S sess_scope
setup和teardown操作:
- setup,在测试函数或类之前执行,完成准备工作,例如数据库链接、测试数据、打开文件等
- teardown,在测试函数或类之后执行,完成收尾工作,例如断开数据库链接、回收内存资源等
- 备注:也可以通过在fixture函数中通过yield实现setup和teardown功能
4. One More Thing
4.1 重复运行
pip install pytest-repeat
# 在执行命令中添加–count=NUM NUM为每条用例需要执行的次数
pytest test_se.py --count=3
4.2 并行运行
pip install pytest-xdist
# pytest-xdist插件的 -n numprocesses 选项可以指定运行测试的处理器进程数,-n auto选项可以自动侦测系统里的CPU数码
pytest -n 3 test_test.py
pytest -n auto test_test.py
4.3 设定执行顺序
pip install pytest-ordering
# 根据order参数从小到大执行
@pytest.mark.run(order=2)
def test_order1():
print ("first test")
assert True
@pytest.mark.run(order=1)
def test_order2():
print ("second test")
assert True
4.4 测试时间限制
在正常时间下,pytest是没有测试时间限制的,但有时候需要控制测试用例执行执行,可以使用pytest-timeout。
pip install pytest-timeout
# 在命令行中添加 –timeout=X,X为该条命令执行的总时间限制,单位秒
pytest --timeout=3.5 test_xxx.py
4.5 常用执行参数
-m: 用表达式指定多个标记名。 pytest 提供了一个装饰器 @pytest.mark.xxx,用于标记测试并分组(xxx是你定义的分组名),以便你快速选中并运行,各个分组直接用 and、or 来分割。
# 首先给测试用例打标签(mark),在 Class、method 上加上如下装饰器:
# @pytest.mark.xxx
# 同时选中带有这两个标签的所有测试用例运行
pytest -m "mark1 and mark2"
# 选中带有mark1的测试用例,不运行mark2的测试用例
pytest -m "mark1 and not mark2"
# 选中带有mark1或 mark2标签的所有测试用例
pytest -m "mark1 or mark2"
-v: 运行时输出更详细的用例执行信息 不使用 -v 参数,运行时不会显示运行的具体测试用例名称;使用 -v 参数,会在 console 里打印出具体哪条测试用例被运行。
-q: 类似 unittest 里的 verbosity,用来简化运行输出信息。 使用 -q 运行测试用例,仅仅显示很简单的运行信息, 例如:
.s.. [100%]
3 passed, 1 skipped in 9.60s
-k: 可以通过表达式运行指定的测试用例。 它是一种模糊匹配,用 and 或 or 区分各个关键字,匹配范围有文件名、类名、函数名。
# 运行test_se.py下的所有的测试
pytest -k "test_se.py"
# 因为se能匹配上test_se.py,故运行test_se.py下所有的测试
pytest -k "se"
# 因为Baidu能匹配上test_baidu.py里定义的测试类Baidu,故运行Baidu测试类下所有的测试,你也可以写成Bai.
pytest -k "Baidu"
-x: 出现一条测试用例失败就退出测试。 在调试时,这个功能非常有用。当出现测试失败时,停止运行后续的测试。
-s: 显示print内容 在运行测试脚本时,为了调试或打印一些内容,我们会在代码中加一些print内容,但是在运行pytest时,这些内容不会显示出来。如果带上-s,就可以显示了。
pytest test_se.py -s
4.6 忽略测试用例执行
4.6.1 直接忽略测试执行
# 直接忽略可以使用 @pytest.mark.skip 装饰器来实现。
@pytest.mark.skip(reason='skip此测试用例')
def test_get_new_message:
pass
4.6.2 按条件忽略测试执行—使用’skipif’忽略
# 按skipif条件,当条件符合时便会忽略某条测试用例执行。
# 定义一个flag,用来指示是否要skip一个测试用例
flag = 1
# 此处判断flag的值,为1则忽略,0则不忽略
@pytest.mark.skipif(flag == 1, reason='by condition')
def test_get_new_message:
pass
4.6.3 xfail 失败后继续执行
在使用pytest时,有些用例我们预计他可能会失败,这个时候就需要使用xfail了,当测试用例失败时,会被跳过:
import pytest
@pytest.mark.xfail()
def test_1():
assert 1 == 2
@pytest.mark.xfail()
def test_2():
assert 1 == 1
if __name__ == '__main__':
pytest.main()
执行后的结果,会使用大小写的xX来表示,算入预期内的通过和不通过。
4.6.4 按条件忽略测试执行—使用’-m’或者’-k’忽略
# 忽略方法名包含method的测试用例
pytest -k "not method"
# 忽略被装饰器@pytest.mark.slow装饰的所有测试用例
pytest -m "not slow"
5. Author: 作者信息
数字老K
quantgalaxy@outlook.com
欢迎交流