Python高级技术之:如何利用`pytest`的`conftest.py`文件,共享`fixture`和配置。

各位观众老爷,晚上好!我是你们的老朋友,今天咱们来聊聊 pytestconftest.py,这玩意儿就像个百宝箱,能帮你共享 fixture 和配置,让你的测试代码更加优雅高效。

为啥需要 conftest.py

想象一下,你有很多测试文件,每个文件都需要用到一些相同的 fixture,比如数据库连接、API 客户端等等。如果每个文件都定义一遍这些 fixture,那简直就是一场噩梦,代码重复不说,维护起来也痛苦不堪。

这时候,conftest.py 就闪亮登场了。它可以让你把这些通用的 fixture 和配置放在一个地方,然后各个测试文件就可以直接使用了,简直不要太方便!

conftest.py 的工作原理

pytest 在运行测试时,会自动查找当前目录和所有父目录下的 conftest.py 文件。它会加载这些文件,然后把里面定义的 fixture 和配置都注册到测试环境中。

简单来说,就是 pytest 会先扫一遍你项目里的 conftest.py 文件,把里面的宝贝都收起来,然后测试文件就可以随意调用了。

conftest.py 的用法详解

咱们先建一个简单的项目目录结构:

my_project/
├── conftest.py
├── test_module1.py
└── test_module2.py
  • conftest.py: 存放共享 fixture 和配置的文件
  • test_module1.py: 测试模块 1
  • test_module2.py: 测试模块 2

1. 定义 fixture

conftest.py 中,我们可以定义各种各样的 fixture

# conftest.py
import pytest

@pytest.fixture
def my_fixture():
    """
    这是一个简单的 fixture,返回一个字符串。
    """
    return "Hello from conftest.py!"

@pytest.fixture(scope="module") # scope 决定了 fixture 的生命周期
def module_fixture():
    """
    一个 module 级别的 fixture,每个 module 只会被调用一次。
    """
    print("nModule fixture setup") # setup
    yield "Module data"
    print("nModule fixture teardown") # teardown

@pytest.fixture(params=[1, 2, 3])
def parameterized_fixture(request):
  """
  一个参数化的 fixture,pytest 会自动运行三次测试,每次使用不同的参数。
  """
  return request.param

解释:

  • @pytest.fixture: 这是定义 fixture 的装饰器。
  • my_fixture: 一个简单的 fixture,返回一个字符串。
  • scope="module": scope 参数决定了 fixture 的生命周期。module 表示这个 fixture 在每个模块(.py 文件)中只会被调用一次。其他的 scope 包括 "function"(默认值,每个测试函数调用一次), "class"(每个类调用一次), "session"(整个测试会话调用一次)。
  • yield: fixture 可以使用 yield 关键字来定义 setup 和 teardown 逻辑。在 yield 之前的代码会在测试函数执行之前运行(setup),yield 之后的代码会在测试函数执行之后运行(teardown)。
  • params=[1, 2, 3]: params 参数定义了参数化 fixture 的参数列表。pytest 会自动运行多次测试,每次使用不同的参数。
  • request: 在参数化 fixture 中,可以使用 request 对象来获取当前的参数值。

2. 使用 fixture

在测试文件中,我们可以直接使用 conftest.py 中定义的 fixture

# test_module1.py
def test_my_fixture(my_fixture):
    """
    测试 my_fixture。
    """
    assert my_fixture == "Hello from conftest.py!"

def test_module_fixture(module_fixture):
    """
    测试 module_fixture。
    """
    assert module_fixture == "Module data"

def test_parameterized_fixture(parameterized_fixture):
    """
    测试 parameterized_fixture.
    """
    print(f"nTesting with parameter: {parameterized_fixture}")
    assert parameterized_fixture in [1, 2, 3]
# test_module2.py
def test_my_fixture_again(my_fixture):
    """
    再次测试 my_fixture。
    """
    assert my_fixture == "Hello from conftest.py!"

def test_module_fixture_again(module_fixture):
    """
    再次测试 module_fixture.
    """
    assert module_fixture == "Module data"

解释:

  • 直接在测试函数的参数列表中声明 fixture 的名字,pytest 就会自动把对应的 fixture 注入到测试函数中。

3. 运行测试

在项目根目录下运行 pytest 命令:

pytest

你会看到类似这样的输出:

============================= test session starts ==============================
platform darwin -- Python 3.9.7, pytest-7.1.2, pluggy-1.0.0
rootdir: /path/to/my_project
collected 7 items

test_module1.py::test_my_fixture PASSED                                [ 14%]
test_module1.py::test_module_fixture
Module fixture setup
 PASSED                                [ 28%]
test_module1.py::test_parameterized_fixture[1]
Testing with parameter: 1
PASSED                                [ 42%]
test_module1.py::test_parameterized_fixture[2]
Testing with parameter: 2
PASSED                                [ 57%]
test_module1.py::test_parameterized_fixture[3]
Testing with parameter: 3
PASSED                                [ 71%]
test_module2.py::test_my_fixture_again PASSED                                [ 85%]
test_module2.py::test_module_fixture_again PASSED                                [100%]

Module fixture teardown

============================== 7 passed in 0.04s ===============================

注意:

  • Module fixture setupModule fixture teardown 只被打印了一次,说明 module_fixture 的作用域是 module 级别的。
  • test_parameterized_fixture 被运行了三次,每次使用不同的参数。

4. 配置选项

conftest.py 还可以用来配置 pytest 的行为。

# conftest.py

def pytest_addoption(parser):
    """
    添加命令行选项。
    """
    parser.addoption(
        "--env",
        action="store",
        default="dev",
        help="运行环境 (dev, test, prod)",
    )

@pytest.fixture
def env(request):
    """
    获取命令行选项的值。
    """
    return request.config.getoption("--env")

def pytest_configure(config):
    """
    在测试运行开始时执行一些配置操作。
    """
    config.addinivalue_line(
        "markers", "slow: marks tests as slow (deselect with '-m "not slow"')"
    )

解释:

  • pytest_addoption: 这个 hook 函数用于添加命令行选项。
    • --env: 添加一个名为 --env 的命令行选项,默认值为 dev,用于指定运行环境。
  • env: 一个 fixture,用于获取命令行选项 --env 的值。
  • pytest_configure: 这个 hook 函数在测试运行开始时执行。
    • config.addinivalue_line("markers", "slow: ..."): 添加一个名为 slow 的 marker。

使用:

  • 运行测试时,可以使用 --env 选项来指定运行环境:

    pytest --env test
  • 可以使用 pytest.mark.slow 装饰器来标记慢速测试:

    # test_module1.py
    import pytest
    
    @pytest.mark.slow
    def test_slow_function():
        """
        这是一个慢速测试。
        """
        import time
        time.sleep(2)
        assert True

    然后可以使用 -m 选项来选择或排除带有特定 marker 的测试:

    pytest -m "slow"  # 只运行标记为 slow 的测试
    pytest -m "not slow"  # 排除标记为 slow 的测试

5. conftest.py 的位置

pytest 会查找当前目录和所有父目录下的 conftest.py 文件。这意味着,你可以把 conftest.py 放在项目根目录下,这样所有的测试文件都可以访问里面的 fixture 和配置。

或者,你也可以把 conftest.py 放在某个子目录下,这样只有该目录及其子目录下的测试文件才能访问里面的 fixture 和配置。这可以帮助你更好地组织和管理你的 fixture 和配置。

6. fixture 的覆盖

如果多个 conftest.py 文件中定义了同名的 fixturepytest 会按照以下优先级来选择使用哪个 fixture

  1. 测试文件所在的目录下的 conftest.py
  2. 测试文件所在的目录的父目录下的 conftest.py
  3. 以此类推,直到项目根目录下的 conftest.py

这意味着,你可以在子目录下的 conftest.py 中覆盖父目录下的 fixture,从而为特定的测试文件提供定制的 fixture

示例:

my_project/
├── conftest.py         (全局配置)
├── test_module1.py
└── sub_directory/
    ├── conftest.py     (子目录特定配置)
    └── test_module2.py

如果 sub_directory/conftest.py 中定义了一个与项目根目录下的 conftest.py 中同名的 fixture,那么 sub_directory/test_module2.py 会使用 sub_directory/conftest.py 中定义的 fixture。而 test_module1.py 仍然会使用项目根目录下的 conftest.py 中定义的 fixture

总结

conftest.pypytest 中一个非常强大的工具,可以帮助你共享 fixture 和配置,让你的测试代码更加简洁、可维护。掌握了 conftest.py 的用法,你的 pytest 水平就能更上一层楼。

表格总结:conftest.py 常用功能

功能 描述 示例
定义共享 fixture 在多个测试文件中共享 fixture,避免代码重复。 python # conftest.py import pytest @pytest.fixture def db_connection(): return "Database Connection" python # test_module.py def test_db(db_connection): assert db_connection == "Database Connection"
配置 pytest 添加命令行选项、注册 marker 等,定制 pytest 的行为。 python # conftest.py def pytest_addoption(parser): parser.addoption("--verbose", action="store_true", help="增加详细输出") def pytest_configure(config): if config.getoption("--verbose"): print("详细模式已开启")
定义 hook 函数 pytest 提供的 hook 函数,可以在测试的不同阶段执行自定义逻辑。 python # conftest.py def pytest_sessionfinish(session, exitstatus): print(f"测试会话结束,退出状态码:{exitstatus}")
fixture 作用域 控制 fixture 的生命周期,例如 function、module、class、session 级别。 python # conftest.py import pytest @pytest.fixture(scope="module") def module_data(): return "Module Data"
参数化 fixture fixture 返回不同的值,从而运行多次测试,每次使用不同的参数。 python # conftest.py import pytest @pytest.fixture(params=[1, 2, 3]) def number(request): return request.param python # test_module.py def test_number(number): print(f"测试数字:{number}") assert number > 0
覆盖 fixture 在子目录下的 conftest.py 中覆盖父目录下的 fixture,为特定的测试文件提供定制的 fixture (见上文示例)

希望今天的讲座能帮到大家! 以后有机会再和大家分享更多 pytest 的实用技巧。 拜拜!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注