1. 问题现象与背景为什么我的setup和teardown不工作了最近在带团队做自动化测试框架升级从unittest迁移到pytest。好几个同事都跑来问我同一个问题“哥我写的测试类里setup_class和teardown_class怎么不执行了代码明明和unittest里长得一样啊” 这确实是个从unittest转向pytest时非常典型的“踩坑”点。表面上看你只是换了个测试运行器但底层pytest的设计哲学和执行机制与unittest有本质区别直接套用老写法自然会碰壁。简单来说在pytest中如果你在测试类里直接定义名为setup、teardown、setup_class、teardown_class的方法pytest默认是不会把它们当作固件Fixture来识别和执行的。pytest有一套自己更强大、更灵活的固件系统它期望你使用pytest.fixture装饰器来明确定义。当你沿用unittest的命名约定时这些方法就被当作普通的实例方法或类方法处理了自然不会在测试前后自动触发。这就像你拿着老式收音机的调频旋钮去操作智能手机虽然旋钮还在但它已经失去了原本的功能。这个问题不解决会直接导致测试环境初始化失败、测试数据清理不到位、资源泄露等一系列连锁反应。比如数据库连接在类开始时没有建立所有依赖数据库的测试用例都会失败或者测试结束后临时文件没有删除跑几次磁盘就满了。所以理解并解决setup/teardown不生效的问题是掌握pytest框架的关键一步。2. 核心原理pytest固件系统 vsunittest生命周期钩子要彻底解决问题得先明白两者设计上的根本差异。unittest源自Java的JUnit采用的是经典的xUnit风格其生命周期钩子方法是基于特定的命名约定如setUp,tearDown,setUpClass,tearDownClass。测试运行器通过反射找到这些特定名称的方法并调用它们。这是一种“约定大于配置”的方式简单直接但不够灵活。而pytest的固件系统是其核心优势之一它采用了一种“依赖注入”的模型。固件本质上是一些函数你用pytest.fixture装饰器标记它们。测试函数通过将固件函数名声明为参数来“请求”使用这些固件。pytest框架负责在运行测试前解析这些依赖执行相应的固件函数并将返回值如果有注入到测试函数中。这种方式带来了极大的灵活性作用域可控一个固件可以定义不同的作用域functionclassmodulesession精确控制其创建和销毁的时机。依赖可组合固件可以依赖其他固件形成清晰的依赖链。可复用性高固件可以定义在conftest.py中供多个测试模块共享。更显式测试函数需要什么资源通过参数列表一目了然。当你写了一个setup_class(self)方法时pytest并不会自动把它看作一个class作用域的固件。它只是一个普通的类方法。pytest的测试发现机制会找到类中以test_开头的方法并执行它们但不会自动去调用那个叫setup_class的方法。这就是问题根源。那么有没有办法让pytest识别这些传统的xUnit风格方法呢答案是肯定的pytest为了兼容提供了这个功能但需要满足特定条件或进行配置这也是很多人忽略的地方。3. 解决方案一启用pytest的xUnit风格兼容模式pytest考虑到了迁移成本内置了对xUnit风格方法的支持。但默认情况下这个支持可能没有完全打开或者你的类没有满足其要求。3.1 确保继承unittest.TestCase这是让pytest识别传统setup/teardown方法的最直接、最可靠的方式。如果你的测试类继承自unittest.TestCasepytest会将其视为一个unittest测试套件并自动调用其对应的生命周期方法。import unittest import pytest class TestOldStyle(unittest.TestCase): # 关键继承 unittest.TestCase classmethod def setUpClass(cls): print(\n 类级别前置setUpClass ) cls.shared_resource 数据库连接 def setUp(self): print(--- 方法级别前置setUp ---) self.data {user: test_user} def test_example_1(self): print(f执行 test_example_1 使用资源: {self.shared_resource}, 数据: {self.data}) assert self.data[user] test_user def test_example_2(self): print(f执行 test_example_2 使用资源: {self.shared_resource}) assert self.shared_resource is not None def tearDown(self): print(--- 方法级别后置tearDown ---) self.data None classmethod def tearDownClass(cls): print( 类级别后置tearDownClass ) cls.shared_resource None if __name__ __main__: # 可以用 unittest 的方式运行 unittest.main() # 也可以用 pytest 命令运行pytest test_file.py运行结果分析当你用pytest命令运行上述文件时输出会严格按照以下顺序 类级别前置setUpClass --- 方法级别前置setUp --- 执行 test_example_1 使用资源: 数据库连接 数据: {user: test_user} --- 方法级别后置tearDown --- --- 方法级别前置setUp --- 执行 test_example_2 使用资源: 数据库连接 --- 方法级别后置tearDown --- 类级别后置tearDownClass 可以看到setUpClass/tearDownClass在整个类开始和结束时各执行一次setUp/tearDown在每个测试方法前后执行。这种方式完美兼容但代价是你的类被绑定在了unittest框架上无法充分利用pytest更先进的特性比如参数化、灵活的固件作用域等。注意即使继承了unittest.TestCasepytest的一些原生特性如pytest.mark.parametrize在该类上也可能无法正常工作或需要特殊处理。这通常被视为一个过渡方案。3.2 使用pytest.mark.usefixtures进行显式调用不推荐用于xUnit方法网上有些文章会提到用pytest.mark.usefixtures装饰器来调用一个名为setup的函数。但这通常指的是调用一个用pytest.fixture装饰的固件而不是直接调用一个普通方法。对于普通的setup(self)方法这样做是无效的。这个装饰器是用来激活固件的不是用来执行普通类方法的。4. 解决方案二拥抱pytest原生固件系统推荐既然用了pytest长远来看学习和使用其原生的固件系统是更佳选择。它更强大、更清晰。下面我们来看看如何用pytest的方式实现类级别和方法级别的初始化和清理。4.1 实现类级别前置后置 (setup_class/teardown_class)在pytest中我们使用作用域为class的固件。import pytest class TestWithPytestFixtures: # 定义一个类作用域的固件替代 setup_class pytest.fixture(scopeclass, autouseTrue) def class_setup_teardown(self): # 这部分相当于 setup_class print(\n pytest类级别前置class_setup_teardown (开始) ) self.class_level_resource Pytest数据库连接 # 使用yield将固件分为前置和后置两部分 yield # yield之后的部分相当于 teardown_class print( pytest类级别后置class_setup_teardown (结束) ) self.class_level_resource None # 定义一个方法作用域的固件替代 setup/teardown pytest.fixture(autouseTrue) # autouseTrue 表示自动使用无需在测试函数参数中声明 def method_setup_teardown(self): # 相当于 setUp print(--- pytest方法级别前置method_setup_teardown (开始) ---) self.method_level_data {status: active} yield # 相当于 tearDown print(--- pytest方法级别后置method_setup_teardown (结束) ---) self.method_level_data None def test_case_one(self): print(f 执行 test_case_one 类资源: {self.class_level_resource} 方法数据: {self.method_level_data}) assert self.class_level_resource Pytest数据库连接 assert self.method_level_data[status] active def test_case_two(self): print(f 执行 test_case_two 类资源: {self.class_level_resource}) # 注意method_level_data 在这里是新的因为每个方法都会重新运行method_setup_teardown assert self.method_level_data[status] active关键点解析pytest.fixture(scopeclass)scopeclass指明这个固件的作用域是整个测试类。它会在该类中第一个测试方法执行前运行一次yield之前的代码并在该类所有测试方法执行后运行yield之后的代码。autouseTrue这个参数非常有用。它表示这个固件会自动应用于该类或模块中的所有测试你不需要在每个测试函数的参数列表中显式地写上它的名字。这非常适合用来替代那些“全局”的setup/teardown操作。yield关键字这是实现后置清理工作的核心。固件函数执行到yield时暂停并将控制权交给测试函数。测试函数执行完毕后再回来执行yield后面的代码。yield也可以返回数据给测试函数这里我们不需要返回值所以直接使用yield。self的使用注意固件函数是类的实例方法第一个参数是self。通过self我们可以将初始化好的资源如class_level_resource绑定到测试类实例上这样同一个类内的所有测试方法都能访问到它。运行结果 pytest类级别前置class_setup_teardown (开始) --- pytest方法级别前置method_setup_teardown (开始) --- 执行 test_case_one 类资源: Pytest数据库连接 方法数据: {status: active} --- pytest方法级别后置method_setup_teardown (结束) --- --- pytest方法级别前置method_setup_teardown (开始) --- 执行 test_case_two 类资源: Pytest数据库连接 --- pytest方法级别后置method_setup_teardown (结束) --- pytest类级别后置class_setup_teardown (结束) 效果和xUnit风格完全一致但这是纯正的pytest方式。4.2 更精细的控制分离前置与后置固件上面的例子将前置和后置写在了同一个固件里用yield分隔。有时为了更清晰或者前后置逻辑没有强依赖关系我们可以拆分成两个独立的固件。import pytest class TestSeparateFixtures: # 独立的类级别前置固件 pytest.fixture(scopeclass, autouseTrue) def class_setup(self): print(\n 独立的类前置执行) self.shared_config {timeout: 30} # 这里没有yield所以这个固件只做初始化 # 注意如果固件没有yield那么它就没有后置部分。 # 我们需要另一个固件来做清理。 # 独立的类级别后置固件通过依赖class_setup确保在其后执行 pytest.fixture(scopeclass, autouseTrue) def class_teardown(self, class_setup): # 依赖class_setup固件 yield # 这里yield前面没有代码表示前置部分什么都不做 print( 独立的类后置执行) self.shared_config None def test_config_available(self): assert self.shared_config[timeout] 30 print( 测试通过)在这个例子中class_teardown固件通过参数列表声明它依赖于class_setup固件。pytest会保证class_setup先执行。class_teardown的yield之前是空所以测试前它不做任何事yield之后执行清理。这种方式结构更清晰但依赖关系需要显式声明。实操心得对于简单的初始化和清理推荐使用yield模式将相关逻辑放在一起更易于阅读和维护。只有当初始化或清理逻辑非常复杂或者需要在不同地方复用其中一部分时才考虑拆分成独立的固件。5. 解决方案三使用pytest内置的setup_method/teardown_method钩子除了兼容模式和原生固件系统pytest还提供了一些与xUnit命名类似但行为略有不同的钩子方法。它们不是固件而是pytest在特定阶段会去调用的特殊方法。class TestPytestXUnitHooks: def setup_method(self, method): 在每个测试方法执行前被调用。 method参数是即将被执行的测试方法对象。 print(f\n--- setup_method for {method.__name__} ---) self.data [1, 2, 3] def teardown_method(self, method): 在每个测试方法执行后被调用。 print(f--- teardown_method for {method.__name__} ---) self.data None classmethod def setup_class(cls): 在测试类开始执行时被调用。注意是类方法。 print( pytest setup_class ) cls.class_var class_scope classmethod def teardown_class(cls): 在测试类所有方法执行完毕后被调用。注意是类方法。 print( pytest teardown_class ) cls.class_var None def test_hook_example(self): print(f 执行测试数据: {self.data} 类变量: {self.class_var}) assert self.data [1, 2, 3] def test_another(self): print(f 执行另一个测试) assert self.class_var class_scope运行结果 pytest setup_class --- setup_method for test_hook_example --- 执行测试数据: [1, 2, 3] 类变量: class_scope --- teardown_method for test_hook_example --- --- setup_method for test_another --- 执行另一个测试 --- teardown_method for test_another --- pytest teardown_class 重要区别与注意事项命名是有效的在这个例子中setup_method,teardown_method,setup_class,teardown_class这些方法被成功调用了。这是因为pytest会自动发现测试类中的这些特定名称的方法注意是类方法并作为钩子执行。与unittest的区别在unittest中setUp和tearDown是实例方法没有参数。在pytest的钩子中setup_method和teardown_method是实例方法但接收一个参数通常命名为method这个参数是当前正在运行的测试方法对象。这有时可以用来做一些基于测试方法名的特殊处理。不是固件这些方法是钩子不是固件。它们不能通过pytest.fixture装饰器来配置作用域也不能被其他测试函数显式请求。它们的行为是固定的。与固件共存如果同时存在同作用域的固件如autouseTrue的function作用域固件和setup_method它们的执行顺序是类作用域固件(yield前) -setup_class- 函数作用域固件(yield前) /setup_method- 测试函数 - 函数作用域固件(yield后) /teardown_method-teardown_class- 类作用域固件(yield后)。具体顺序可能受依赖关系影响比较复杂。避坑指南我个人的建议是在纯粹的pytest项目中尽量避免混合使用钩子方法和固件系统。选择其中一种并坚持使用通常是原生的固件系统可以使代码更一致、更可预测。混合使用会让测试的执行流程变得难以理解和调试。6. 深度排查当所有方法都失效时的检查清单如果你的setup/teardown仍然不生效请按照以下清单逐一排查6.1 检查点一类名与方法命名类名是否以Test开头pytest默认的测试发现规则是查找以Test开头的类且不能有__init__方法或者以test_开头的函数。如果你的类叫MyTestClass需要修改为TestMyClass或者在pytest.ini配置文件中修改python_classes配置。测试方法是否以test_开头只有以test_开头的方法才会被识别为测试用例。setup_class本身不是测试用例它只会在测试方法被执行时由框架触发。是否误用了__init__方法在测试类中定义__init__方法会干扰pytest的实例化过程可能导致各种奇怪的问题包括固件不执行。绝对不要在测试类中定义__init__方法。初始化的逻辑请放在固件或setup_class中。6.2 检查点二作用域与依赖固件作用域是否正确如果你想实现类级别的初始化固件作用域必须是scopeclass。如果误写为scopefunction默认值它会在每个测试方法前后都执行。autouseTrue用对了吗如果你没有在测试函数参数中请求固件那么必须为固件加上autouseTrue否则它不会被自动执行。固件依赖是否正确如果一个后置固件依赖于一个前置固件如上面的class_teardown依赖class_setup必须在前者的参数列表中声明。6.3 检查点三执行命令与配置是否在正确的目录下执行pytest确保你的命令行当前目录包含了你的测试文件或者使用pytest path/to/your/test_file.py指定路径。是否使用了pytest命令而非python -m unittest这听起来很基础但确实有人会弄混。确保你运行的是pytest。检查pytest.ini或pyproject.toml配置某些配置可能会影响测试收集和行为。例如addopts --tbshort不会影响固件执行但如果是自定义的插件或钩子可能会。6.4 检查点四使用pytest的--setup-show选项进行调试这是最强大的调试工具。在运行测试时加上-s禁用输出捕获和--setup-show选项可以清晰地看到固件的执行顺序和范围。pytest your_test_file.py -s --setup-show输出会类似这样SETUP F class_setup_teardown (class) SETUP F method_setup_teardown (function) 你的测试文件.py::TestWithPytestFixtures::test_case_one (fixtures used: class_setup_teardown, method_setup_teardown) TEARDOWN F method_setup_teardown SETUP F method_setup_teardown (function) 你的测试文件.py::TestWithPytestFixtures::test_case_two (fixtures used: class_setup_teardown, method_setup_teardown) TEARDOWN F method_setup_teardown TEARDOWN F class_setup_teardown通过这个输出你可以一目了然地看到哪些固件被使用了fixtures used:后面列出。固件的执行顺序SETUP和TEARDOWN。固件的作用域(class),(function)。如果在这个列表里看不到你定义的setup或teardown方法对应的固件那就说明它们没有被pytest识别为固件你需要回头检查命名、装饰器或继承关系。7. 实战案例从“不生效”到“优雅生效”的重构让我们看一个完整的、从问题代码到优化代码的案例。问题代码unittest风格在pytest下可能不生效或表现不佳# test_problematic.py class TestUserService: # 类名不以Test开头pytest默认可能不收集 def setup(self): # 可能不被识别为固件 self.user_client UserClient() self.test_data {name: alice} def teardown(self): # 可能不被识别为固件 self.user_client.close() def test_create_user(self): resp self.user_client.create(self.test_data) assert resp.status_code 201 # ... 其他断言步骤1确保测试被收集将类名改为以Test开头。class TestUserService: # 改为 TestUserService步骤2迁移到pytest原生固件系统方案二这是最推荐的做法能获得pytest的全部能力。import pytest class TestUserService: pytest.fixture(autouseTrue) # 添加装饰器声明为自动使用的固件 def setup_teardown(self): # 前置初始化 self.user_client UserClient() self.test_data {name: alice} print(Fixture: 资源初始化完成) # yield 分隔前后置 yield # 后置清理 self.user_client.close() print(Fixture: 资源清理完成) def test_create_user(self): # 现在可以安全地使用 self.user_client 和 self.test_data resp self.user_client.create(self.test_data) assert resp.status_code 201步骤3优化与扩展现在我们可以利用pytest的特性进行优化。添加类级别共享资源class TestUserService: pytest.fixture(scopeclass, autouseTrue) def class_level_fixture(self): 模拟一个昂贵的、类级别共享的连接池 print(\n 初始化类级别连接池) cls.connection_pool MockConnectionPool(size5) yield print( 关闭类级别连接池) cls.connection_pool.shutdown() pytest.fixture(autouseTrue) def method_level_fixture(self): 每个测试方法获取一个连接用完后归还 print( -- 从池中获取连接) self.conn self.connection_pool.get_connection() yield print( -- 归还连接到池) self.connection_pool.release_connection(self.conn) def test_query_1(self): result self.conn.query(SELECT 1) assert result is not None def test_query_2(self): # 使用同一个连接池但不同的连接对象 result self.conn.query(SELECT 2) assert result is not None使用参数化减少重复pytest.mark.parametrize(username, expected_status, [ (alice, 201), (bob*20, 400), # 超长用户名 (, 422), # 空用户名 ]) def test_create_user_with_params(self, username, expected_status): 一个测试方法覆盖多种边界情况 resp self.user_client.create({name: username}) assert resp.status_code expected_status通过这样的重构代码不仅解决了setup/teardown不生效的问题而且变得更模块化、更可读、更强大。固件系统使得资源管理清晰明了参数化大幅减少了重复代码这才是pytest带来的真正生产力提升。