ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

使用 pytest 运行 unittest 风格测试:无缝兼容与渐进迁移实战指南

使用 pytest 运行 unittest 风格测试:无缝兼容与渐进迁移实战指南 使用 pytest 运行 unittest 风格测试无缝兼容与渐进迁移实战指南【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest本指南围绕 pytest 内置的 unittest 兼容机制展开说明如何直接以 pytest 作为测试运行器来执行既有unittest风格的测试套件并在不修改源码的前提下享受断言重写、输出捕获、失败调试与并行执行等能力。读完本文你将掌握 unittest 用例在 pytest 中的收集与执行规则、skip/xfail/subTest等特性的兼容情况、通过usefixtures与autouse向TestCase子类注入 pytest fixture 的具体做法以及逐步把旧套件迁移到纯 pytest 风格的演进路径。概览为什么在 pytest 中运行 unittest 测试pytest 开箱即用地支持运行 Python 标准库unittest风格的测试这是项目文档 doc/en/how-to/unittest.rst 明确承诺的能力。设计目标有两个作为运行器直接执行既有unittest套件不需要改写测试代码允许增量改造让套件逐步拥抱 pytest 的完整特性。运行方式极其简单在包含test_*.py或*_test.py文件的目录上直接执行pytest testspytest 会自动收集这些文件中的unittest.TestCase子类及其test*方法。unittest 特性的支持矩阵完整支持的标准库特性根据官方文档几乎所有常用的unittest特性都能在 pytest 下正常工作特性说明unittest.skip/unittest.skipIf装饰器跳过单个测试等价语义被转换为 pytest 的 skipsetUp/tearDown每个测试方法前后的实例级准备与清理setUpClass/tearDownClass类级别的一次性准备与清理setUpModule/tearDownModule模块级别的准备与清理TestCase.subTest子测试支持自 pytest 9.0 起原生集成这些能力在仓库测试 testing/test_unittest.py 中均有对应验证例如test_simple_unittest、test_setup、test_setUpModule、test_setup_setUpClass等用例分别覆盖了方法级、模块级与类级 setup/teardown 的执行顺序与失败上报。暂不支持的load_tests协议截至当前版本pytest尚未支持Python 标准库的load_tests协议loadTestsFromModule相关的自定义收集钩子。如果测试套件依赖该协议来注入或过滤用例需要继续沿用unittest原生的运行方式。支持与不支持的 pytest 特性在unittest.TestCase子类中可以使用的 pytest 特性Marksskip、skipif、xfail等标记Auto-use fixtures自动使用的 fixture见下文专节。以下 pytest 特性不生效官方文档明确说明由于两个框架设计哲学不同可能永远都不会支持Fixtures除autouse外作为测试方法参数的 fixture 注入Parametrization参数化Custom hooks自定义插件钩子。第三方插件能否与 unittest 风格套件协同工作取决于具体插件与套件的实现无法一概而论。开箱即得的收益不修改任何既有代码仅切换到 pytest 运行即可获得更易读的失败信息断言失败时展示更详细的上下文与差异对比参考 doc/en/how-to/failures.rststdout/stderr 捕获默认捕获测试输出仅在失败时展示避免刷屏参考 doc/en/how-to/capture-stdout-stderr.rst测试选择能力-k按关键字表达式筛选与-m按标记筛选参考 doc/en/how-to/usage.rst--maxfailN失败 N 个测试后即停止--pdb测试失败时进入调试器注意下文关于执行阶段的说明pytest-xdist插件将测试分发到多个 CPU 并行执行plainassert用普通assert语句取代self.assert*系列方法unittest2pytest工具可极大简化这类改写断言失败时 pytest 会自动显示表达式求值细节参考 doc/en/how-to/assert.rst。收集与执行的源码级原理unittest 支持并非魔法而是由内置插件 src/_pytest/unittest.py 实现的。理解其原理有助于排查收集与执行阶段的疑难问题。收集阶段钩子pytest_pycollect_makeitemsrc/_pytest/unittest.py负责识别并生成收集项检查sys.modules中是否已导入unittest且目标对象是否为unittest.TestCase的子类通过inspect.isabstract排除抽象类抽象类无法实例化收集没有意义满足条件后创建UnitTestCase收集器。UnitTestCase.collect()src/_pytest/unittest.py再借助标准库的TestLoader().getTestCaseNames()枚举test*方法并逐个生成TestCaseFunction。两个细节值得注意若类上定义了__test__ False该类的用例不会被收集若类没有匹配到任何test*方法但定义了runTest则会收集一个名为runTest的用例test_runTest_method测试用例在 testing/test_unittest.py 验证了这一行为。实例创建标准unittest中每个测试方法使用新实例执行pytest 保持了这一语义。UnitTestCase.newinstance()以runTest作为构造参数创建哑实例见 src/_pytest/unittest.py注释指出TestCase.__init__将runTest视为特殊的空操作名称可用于需要哑实例的场景。testing/test_unittest.py中的test_new_instances用例专门验证了这一点同一类中的多个测试方法拥有各自独立的实例。setup/teardown 的桥接实现UnitTestCase.collect()内部通过fixtures.register_fixture注册了多个自动使用的隐藏 fixturesrc/_pytest/unittest.py把标准库的生命周期方法桥接到 pytest 的 fixture 体系_unittest_setUpClass_fixture_类名scopeclass在类级调用setUpClass/tearDownClass/doClassCleanups对应 issue #517_unittest_setup_method_fixture_类名scopefunction调用非标准但常见的setup_method/teardown_method_unittest_skip_fixture_类名当类被unittest.skip/unittest.skipIf装饰时自动抛出 pytest 的 skip对应 issue #13885。因此setUpClass、tearDownClass等方法的异常也能被 pytest 正常捕获与上报test_setup_setUpClass、test_fixtures_setup_setUpClass_issue8394等用例均覆盖了这些路径。执行阶段TestCaseFunction.runtest()src/_pytest/unittest.py构造TestCase实例并调用testcase(resultself)——即让 pytest 自身充当unittest的TestResult对象从而接收addError、addFailure、addSkip、addExpectedFailure、addUnexpectedSuccess、addSubTest等回调并逐一转换为 pytest 的 outcomes。值得展开的几个转换细节跳过addSkip抛出pytest.skip异常pytest_runtest_makereport钩子src/_pytest/unittest.py还会把显式raise unittest.SkipTest也转换为pytest.skip确保两种跳过方式表现一致预期失败addExpectedFailure转换为xfailaddUnexpectedSuccess则保持unittest语义——明确判定为失败而非 XPASS见 src/_pytest/unittest.py。testing/test_unittest.py 的test_unittest_expected_failure_for_failing_test_is_xfail用例同时用 pytest 与unittest.main()运行同一脚本并断言结果一致回调查看pytest 的TestCaseFunction实现了标准库的TestResult协议方法startTest、stopTest、addDuration、addSuccess等因此可直接充当unittest的 result 接收者src/_pytest/unittest.py。执行阶段差异的重要提醒官方文档有一则关键note由于两个框架的架构差异unittest 风格测试的 setup 与 teardown 是在 pytest 的call阶段执行的而不是在 pytest 标准的setup与teardown阶段。这会带来一个容易误判的现象如果unittest套件在 setup 阶段出错pytest 在其setup阶段不会报告任何错误而是把这个错误延迟到call阶段抛出。排查问题时请留意这一点尤其是结合--pdb调试 setup 阶段失败时runtest()中的注释说明了--pdb下延迟tearDown调用的设计意图见 src/_pytest/unittest.py。用 marks 向 TestCase 子类注入 pytest fixture虽然unittest.TestCase方法不能直接把 fixture 作为参数接收但 pytest 提供了官方的混用方案pytest.mark.usefixtures类装饰器。完整的可运行示例来自官方文档分为两个文件。首先是conftest.py定义一个scopeclass的 fixture把类级缓存的数据库对象挂到请求它的测试类上# content of conftest.py import pytest pytest.fixture(scopeclass) def db_class(request): class DummyDB: pass # set a class attribute on the invoking test context request.cls.db DummyDB()这里的核心是request对象fixture 通过request.cls拿到请求该 fixture 的测试类并把db属性直接写到类上。这种设计把 fixture 的编写与具体测试代码解耦测试端只需按名字引用一次。然后是实际的TestCase子类# content of test_unittest_db.py import unittest import pytest pytest.mark.usefixtures(db_class) class MyTest(unittest.TestCase): def test_method1(self): assert hasattr(self, db) assert 0, self.db # fail for demo purposes def test_method2(self): assert 0, self.db # fail for demo purposespytest.mark.usefixtures(db_class)保证db_classfixture 每个测试类恰好执行一次。两个方法里的断言被故意写成失败从而在 traceback 中观察self.db的值$ pytest test_unittest_db.py test session starts platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project collected 2 items test_unittest_db.py FF [100%] FAILURES ___________________________ MyTest.test_method1 ____________________________ self test_unittest_db.MyTest testMethodtest_method1 def test_method1(self): assert hasattr(self, db) assert 0, self.db # fail for demo purposes ^^^^^^^^^^^^^^^^^ E AssertionError: conftest.db_class.locals.DummyDB object at 0xdeadbeef0001 E assert 0 test_unittest_db.py:11: AssertionError ___________________________ MyTest.test_method2 ____________________________ self test_unittest_db.MyTest testMethodtest_method2 def test_method2(self): assert 0, self.db # fail for demo purposes ^^^^^^^^^^^^^^^^^ E AssertionError: conftest.db_class.locals.DummyDB object at 0xdeadbeef0001 E assert 0 test_unittest_db.py:14: AssertionError short test summary info FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: conft... FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: conft... 2 failed in 0.12s 两个 traceback 中的self.db指向同一个DummyDB实例内存地址0xdeadbeef0001一致这正是类级作用域 fixture 的预期效果——数据库对象在类内被所有测试方法共享。默认的 pytest traceback 也顺带展示了断言重写带来的信息量提升失败时直接打印出被断言的表达式self.db的求值结果。autouse fixture隐式地注入准备逻辑有些场景更适合隐式的准备逻辑——这与传统unittest的setUp风格一脉相承。将 fixture 标记为pytest.fixture(autouseTrue)后它会在定义它的上下文中自动生效等价于在类上加pytest.mark.usefixtures(initdir)。下面的例子来自官方文档initdirfixture 让类中所有测试方法都在一个预置了samplefile.ini的临时目录中执行它自身则委托给 pytest 内置的tmp_pathfixture以及monkeypatch的chdir方法来完成目录切换# content of test_unittest_cleandir.py import unittest import pytest class MyTest(unittest.TestCase): pytest.fixture(autouseTrue) def initdir(self, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory tmp_path.joinpath(samplefile.ini).write_text(# testdata, encodingutf-8) def test_method(self): with open(samplefile.ini, encodingutf-8) as f: s f.read() assert testdata in sautouse标志使得initdir对该类所有方法自动生效免去了显式标记。注意方法被定义为TestCase实例方法带self且 fixture 参数tmp_path、monkeypatch不是作为测试方法参数注入的而是作为 fixture 自身的参数被 pytest 解析——这是 autouse 方式可行的关键。运行结果符合预期initdir在test_method之前执行完毕$ pytest -q test_unittest_cleandir.py . [100%] 1 passed in 0.12s官方限制方法不能直接接收 fixture 参数unittest.TestCase的测试方法不能像普通 pytest 测试函数那样直接声明 fixture 参数并让 pytest 注入。官方文档明确说明实现这种注入很可能损害运行通用 unittest.TestCase 套件的能力因此推荐以上述usefixtures与autouse两种方式混入 pytest fixture。subTest 子测试支持pytest 9.0自 pytest 9.0 起unittest.TestCase.subTest获得原生支持其实现位于 src/_pytest/subtests.py。pytest 作为TestResult收到addSubTest回调后会为每个子测试生成独立的SubtestReport并以SUBFAILED、SUBPASSED、SUBSKIPPED等状态单独上报见 src/_pytest/unittest.py 与 src/_pytest/subtests.py。关键的语义保证是只要某个顶层测试内包含失败的子测试即使顶层测试自身通过了整体仍判定为失败——pytest_report_teststatus钩子会统计失败的子测试数并把顶层测试的结果改写为失败见 src/_pytest/subtests.py。testing/test_subtests.py 的TestUnittestSubTest系列用例覆盖了子测试失败、全部通过、以及子测试内skipTest三种场景。一个包含子测试失败的示例from unittest import TestCase class T(TestCase): def test_foo(self): with self.subTest(foo subtest): assert False, foo subtest failure def test_bar(self): with self.subTest(bar subtest): assert False, bar subtest failure assert False, test_bar also failed def test_zaz(self): with self.subTest(zaz subtest): pass子测试报告中的描述由SubtestReport._sub_test_description()生成src/_pytest/subtests.py包含msg参数如[foo subtest]与**kwargs参数如(i3)帮助精确定位是哪一次迭代失败。其他值得注意的兼容细节失败 traceback 过滤TestCaseFunction._traceback_filtersrc/_pytest/unittest.py会过滤掉来自unittest内部框架的帧避免 traceback 中出现failUnlessEqual等底层辅助方法test_unittest_not_shown_in_traceback用例验证了这一点模块级pytestmark在模块顶部声明pytestmark pytest.mark.xxx对TestCase子类同样生效testing/test_unittest.py 的test_module_level_pytestmark用例给出了验证__test__ False类或方法可通过设置__test__ False阻止 pytest 收集Twisted trial 集成当twisted.trial在运行时被激活pytest 的TestCaseFunction会实现IReporter接口并针对不同 Twisted 版本提供异常信息转换的兼容处理src/_pytest/unittest.py依赖 Twisted 的套件可以平顺迁移unittest.expectedFailure失败时上报为 xfail、意外通过时上报为失败语义与unittest.main()完全一致见 testing/test_unittest.py。渐进迁移路径从 unittest 到纯 pytest官方文档给出了一条渐进式演进建议同样适用于你手头的存量套件阶段一零改动接入直接pytest tests运行既有套件先享受输出捕获、-k/-m筛选、--pdb、--maxfail与并行分发pytest-xdist的即时收益阶段二混用 fixture用pytest.mark.usefixtures(...)与pytest.fixture(autouseTrue)逐步把共享准备逻辑重构为 pytest fixture获得更精细的作用域function/class/module/session控制阶段三脱离 TestCase逐步把unittest.TestCase子类改写成普通测试函数与 plainassertunittest2pytest可辅助机械化改写此时即可使用参数化、按需注入的 fixture、自定义插件钩子等完整 pytest 特性。每一步都可在保持套件可运行的前提下独立推进从而在不中断 CI 的情况下完成框架演进。【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表