<p align="center"> <br>做者:HelloGitHub-<strong>Prodesire</strong><br> </p>html
HelloGitHub 的《讲解开源项目》系列,项目地址:https://github.com/HelloGitHub-Team/Articlepython
本篇文章是《聊聊 Python 的单元测试框架》的第三篇,前两篇分别介绍了标准库 unittest 和第三方单元测试框架 nose。做为本系列的最后一篇,压轴出场的是Python 世界中最火的第三方单元测试框架:pytest。git
pytest 项目地址:https://github.com/pytest-dev/pytestgithub
它有以下主要特性:编程
self.assert*
名称了)unittest
彻底兼容,对 nose
基本兼容和前面介绍 unittest
和 nose
同样,咱们将从以下几个方面介绍 pytest
的特性。bash
同 nose
同样,pytest
支持函数、测试类形式的测试用例。最大的不一样点是,你能够尽情地使用 assert
语句进行断言,丝绝不用担忧它会在 nose
或 unittest
中产生的缺失详细上下文信息的问题。session
好比下面的测试示例中,故意使得 test_upper
中断言不经过:架构
import pytest def test_upper(): assert 'foo'.upper() == 'FOO1' class TestClass: def test_one(self): x = "this" assert "h" in x def test_two(self): x = "hello" with pytest.raises(TypeError): x + []
而当使用 pytest
去执行用例时,它会输出详细的(且是多种颜色)上下文信息:app
=================================== test session starts =================================== platform darwin -- Python 3.7.1, pytest-4.0.1, py-1.7.0, pluggy-0.8.0 rootdir: /Users/prodesire/projects/tests, inifile: plugins: cov-2.6.0 collected 3 items test.py F.. [100%] ======================================== FAILURES ========================================= _______________________________________ test_upper ________________________________________ def test_upper(): > assert 'foo'.upper() == 'FOO1' E AssertionError: assert 'FOO' == 'FOO1' E - FOO E + FOO1 E ? + test.py:4: AssertionError =========================== 1 failed, 2 passed in 0.08 seconds ============================
不难看到,pytest
既输出了测试代码上下文,也输出了被测变量值的信息。相比于 nose
和 unittest
,pytest
容许用户使用更简单的方式编写测试用例,又能获得一个更丰富和友好的测试结果。框架
unittest
和 nose
所支持的用例发现和执行能力,pytest
均支持。 pytest
支持用例自动(递归)发现:
test_*.py
或 *_test.py
的测试用例文件中,以 test
开头的测试函数或以 Test
开头的测试类中的以 test
开头的测试方法
pytest
命令nose2
的理念同样,经过在配置文件中指定特定参数,可配置用例文件、类和函数的名称模式(模糊匹配)pytest
也支持执行指定用例:
pytest /path/to/test/file.py
pytest /path/to/test/file.py:TestCase
pytest another.test::TestClass::test_method
pytest /path/to/test/file.py:test_function
pytest
的测试夹具和 unittest
、nose
、nose2
的风格迥异,它不但能实现 setUp
和 tearDown
这种测试前置和清理逻辑,还其余很是多强大的功能。
pytest
中的测试夹具更像是测试资源,你只需定义一个夹具,而后就能够在用例中直接使用它。得益于 pytest
的依赖注入机制,你无需经过from xx import xx
的形式显示导入,只须要在测试函数的参数中指定同名参数便可,好比:
import pytest @pytest.fixture def smtp_connection(): import smtplib return smtplib.SMTP("smtp.gmail.com", 587, timeout=5) def test_ehlo(smtp_connection): response, msg = smtp_connection.ehlo() assert response == 250
上述示例中定义了一个测试夹具 smtp_connection
,在测试函数 test_ehlo
签名中定义了同名参数,则 pytest
框架会自动注入该变量。
在 pytest
中,同一个测试夹具可被多个测试文件中的多个测试用例共享。只需在包(Package)中定义 conftest.py
文件,并把测试夹具的定义写在该文件中,则该包内全部模块(Module)的全部测试用例都可使用 conftest.py
中所定义的测试夹具。
好比,若是在以下文件结构的 test_1/conftest.py
定义了测试夹具,那么 test_a.py
和 test_b.py
可使用该测试夹具;而 test_c.py
则没法使用。
`-- test_1 | |-- conftest.py | `-- test_a.py | `-- test_b.py `-- test_2 `-- test_c.py
unittest
和 nose
均支持测试前置和清理的生效级别:测试方法、测试类和测试模块。
pytest
的测试夹具一样支持各种生效级别,且更加丰富。经过在 pytest.fixture 中指定 scope
参数来设置:
当咱们指定生效级别为模块级时,示例以下:
import pytest import smtplib @pytest.fixture(scope="module") def smtp_connection(): return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
pytest
的测试夹具也可以实现测试前置和清理,经过 yield
语句来拆分这两个逻辑,写法变得很简单,如:
import smtplib import pytest @pytest.fixture(scope="module") def smtp_connection(): smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5) yield smtp_connection # provide the fixture value print("teardown smtp") smtp_connection.close()
在上述示例中,yield smtp_connection
及前面的语句至关于测试前置,经过 yield
返回准备好的测试资源 smtp_connection
; 然后面的语句则会在用例执行结束(确切的说是测试夹具的生效级别的声明周期结束时)后执行,至关于测试清理。
若是生成测试资源(如示例中的 smtp_connection
)的过程支持 with
语句,那么还能够写成更加简单的形式:
@pytest.fixture(scope="module") def smtp_connection(): with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection: yield smtp_connection # provide the fixture value
pytest
的测试夹具除了文中介绍到的这些功能,还有诸如参数化夹具、工厂夹具、在夹具中使用夹具等更多高阶玩法,详情请阅读 "pytest fixtures: explicit, modular, scalable"。
pytest
除了支持 unittest
和 nosetest
的跳过测试和预计失败的方式外,还在 pytest.mark
中提供对应方法:
示例以下:
@pytest.mark.skip(reason="no way of currently testing this") def test_mark_skip(): ... def test_skip(): if not valid_config(): pytest.skip("unsupported configuration") @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") def test_mark_skip_if(): ... @pytest.mark.xfail def test_mark_xfail(): ...
关于跳过测试和预计失败的更多玩法,参见 "Skip and xfail: dealing with tests that cannot succeed"
pytest
除了支持 unittest
中的 TestCase.subTest
,还支持一种更为灵活的子测试编写方式,也就是 参数化测试
,经过 pytest.mark.parametrize
装饰器实现。
在下面的示例中,定义一个 test_eval
测试函数,经过 pytest.mark.parametrize
装饰器指定 3 组参数,则将生成 3 个子测试:
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected
示例中故意让最后一组参数致使失败,运行用例能够看到丰富的测试结果输出:
========================================= test session starts ========================================= platform darwin -- Python 3.7.1, pytest-4.0.1, py-1.7.0, pluggy-0.8.0 rootdir: /Users/prodesire/projects/tests, inifile: plugins: cov-2.6.0 collected 3 items test.py ..F [100%] ============================================== FAILURES =============================================== __________________________________________ test_eval[6*9-42] __________________________________________ test_input = '6*9', expected = 42 @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): > assert eval(test_input) == expected E AssertionError: assert 54 == 42 E + where 54 = eval('6*9') test.py:6: AssertionError ================================= 1 failed, 2 passed in 0.09 seconds ==================================
若将参数换成 pytest.param
,咱们还能够有更高阶的玩法,好比知道最后一组参数是失败的,因此将它标记为 xfail:
@pytest.mark.parametrize( "test_input,expected", [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)], ) def test_eval(test_input, expected): assert eval(test_input) == expected
若是测试函数的多个参数的值但愿互相排列组合,咱们能够这么写:
@pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): pass
上述示例中会分别把 x=0/y=2
、x=1/y=2
、x=0/y=3
和x=1/y=3
带入测试函数,视做四个测试用例来执行。
pytest
的测试结果输出相比于 unittest
和 nose
来讲更为丰富,其优点在于:
pytest
的插件十分丰富,并且即插即用,做为使用者不须要编写额外代码。关于插件的使用,参见"Installing and Using plugins"。
此外,得益于 pytest
良好的架构设计和钩子机制,其插件编写也变得容易上手。关于插件的编写,参见"Writing plugins"。
三篇关于 Python 测试框架的介绍到这里就要收尾了。写了这么多,各位看官怕也是看得累了。咱们不妨罗列一个横向对比表,来总结下这些单元测试框架的异同:
unittest | nose | nose2 | pytest | |
---|---|---|---|---|
自动发现用例 | ✔ | ✔ | ✔ | ✔ |
指定(各级别)用例执行 | ✔ | ✔ | ✔ | ✔ |
支持 assert 断言 | 弱 | 弱 | 弱 | 强 |
测试夹具 | ✔ | ✔ | ✔ | ✔ |
测试夹具种类 | 前置和清理 | 前置和清理 | 前置和清理 | 前置、清理、内置各种 fixtures,自定义各种 fixtures |
测试夹具生效级别 | 方法、类、模块 | 方法、类、模块 | 方法、类、模块 | 方法、类、模块、包、会话 |
支持跳过测试和预计失败 | ✔ | ✔ | ✔ | ✔ |
子测试 | ✔ | ✔ | ✔ | ✔ |
测试结果输出 | 通常 | 较好 | 较好 | 好 |
插件 | - | 较丰富 | 通常 | 丰富 |
钩子 | - | - | ✔ | ✔ |
社区生态 | 做为标准库,由官方维护 | 中止维护 | 维护中,活跃度低 | 维护中,活跃度高 |
Python 的单元测试框架看似种类繁多,实则是一代代的进化,有迹可循。抓住其特色,结合使用场景,就能容易的作出选择。
若你不想安装或不容许第三方库,那么 unittest
是最好也是惟一的选择。反之,pytest
无疑是最佳选择,众多 Python 开源项目(如大名鼎鼎的 requests)都是使用 pytest
做为单元测试框架。甚至,连 nose2
在官方文档上都建议你们使用 pytest
,这得是多大的敬佩呀!
『讲解开源项目系列』——让对开源项目感兴趣的人再也不畏惧、让开源项目的发起者再也不孤单。跟着咱们的文章,你会发现编程的乐趣、使用和发现参与开源项目如此简单。欢迎留言联系咱们、加入咱们,让更多人爱上开源、贡献开源~
原文出处:https://www.cnblogs.com/xueweihan/p/11574791.html