如何将fixture生成的数据提供给测试函数的参数化?如果不行,有什么替代方案吗?

2 投票
1 回答
32 浏览
提问于 2025-04-14 17:23

我需要创建多个用户(通过发送HTTP请求),并使用这些用户的数据(登录名和密码)来测试登录页面。

我有一个工具,它可以生成用户并提供他们的登录信息(像这样的一系列元组:[(登录名1, 密码1), (登录名2, 密码2)])。我想把这些数据用作参数,因为我只知道一种正确的方法,可以用不同的测试数据多次运行一个测试。

以下是代码:

conftest.py

@pytest.fixture(scope="session", autouse=True)
def test_user_fixture():
    print("INFO | Generating test user data")
    user_data_set = user_data_generator()
    login_data = []

    for user_data in user_data_set:
        login_data.append((user_data[0], user_data[1]))
        if send_user_create_request(user_data) != 200: # this function sends request to create user AND returns status_code
            pytest.exit("ERROR | Test user wasn't created")

    yield login_data

    print("INFO | Clearing test data")

test_login_page.py

@pytest.mark.usefixtures('webdriver_fixture', 'test_user_fixture')
class TestPositiveLogin:

    @pytest.mark.parametrize("login, password", test_user_fixture)
    def test_positive_login(self, webdriver_fixture, login, password):
        driver = webdriver_fixture
        page = BasePage(driver)
        page.open_base_page()
        page.login(login, password)

在这里,我尝试直接使用这个工具作为参数,因为登录数据完全符合参数的数据格式,但Python却提示:

NameError: name 'test_user_fixture' is not defined

你能帮我解决这个问题吗,或者给我其他的解决方案吗?

1 个回答

1

你现在的 test_user_fixture 这个设置返回的是一个登录名和密码的列表。我相信你使用 yield 是因为你想在后面做一些清理工作(比如删除用户)。

我建议 test_user_fixture 只返回一个登录名和密码。然后我们可以用 params= 这个关键词来给这个设置添加参数:

# conftest.py
import logging

import pytest

def user_data_generator():
    """Mocked"""
    return [
        ("user1", "password1"),
        ("user2", "password2"),
    ]

USERS_DATA = list(user_data_generator())

def test_id(user_data):
    """Given (login, password), return the login part.

    We use this login as part of the test ID
    """
    return user_data[0]


@pytest.fixture(scope="session", autouse=True, params=USERS_DATA, ids=test_id)
def test_user_fixture(request):
    login, password = request.param[:2]
    logging.debug("In test_user_fixture, login=%r, password=%r", login, password)

    if send_user_create_request((login, password)) != 200:
        pytest.exit("ERROR | Test user wasn't created")
    yield login, password

    logging.info("Clearing test data")

还有

# test_login_page.py
import logging


class TestPositiveLogin:
    def test_positive_login(self, webdriver_fixture, test_user_fixture):
        login, password = test_user_fixture
        logging.debug("webdriver_fixture=%r", webdriver_fixture)
        logging.debug("login=%r", login)
        logging.debug("password=%r", password)

还有

# pyproject.toml
[tool.pytest.ini_options]
log_cli="true"
log_level="DEBUG"

log_cli="false" 时的输出:

test_login_page.py::TestPositiveLogin::test_positive_login[user1] PASSED
test_login_page.py::TestPositiveLogin::test_positive_login[user2] PASSED

log_cli="true" 时的输出:


test_login_page.py::TestPositiveLogin::test_positive_login[user1]
---------------------------------------------------------------------- live log setup ----------------------------------------------------------------------
DEBUG    root:conftest.py:26 In test_user_fixture, login='user1', password='password1'
---------------------------------------------------------------------- live log call -----------------------------------------------------------------------
DEBUG    root:test_login_page.py:8 webdriver_fixture='Mocked WebDriver'
DEBUG    root:test_login_page.py:9 login='user1'
DEBUG    root:test_login_page.py:10 password='password1'
PASSED
test_login_page.py::TestPositiveLogin::test_positive_login[user2]
---------------------------------------------------------------------- live log setup ----------------------------------------------------------------------
INFO     root:conftest.py:32 Clearing test data for login='user1'
DEBUG    root:conftest.py:26 In test_user_fixture, login='user2', password='password2'
---------------------------------------------------------------------- live log call -----------------------------------------------------------------------
DEBUG    root:test_login_page.py:8 webdriver_fixture='Mocked WebDriver'
DEBUG    root:test_login_page.py:9 login='user2'
DEBUG    root:test_login_page.py:10 password='password2'
PASSED
-------------------------------------------------------------------- live log teardown ---------------------------------------------------------------------
INFO     root:conftest.py:32 Clearing test data for login='user2'

注意事项

  • 我假设 USERS_DATA 包含 [(user, password, ...), ...]

  • 这个 params=USERS_DATA 是用来给设置添加参数的。虽然这个设置的作用范围是整个会话,但它会对 USERS_DATA 中的每个元素调用一次。换句话说,如果 USERS_DATA 有两个元素,这个设置就会被调用两次。

  • test_id() 函数从测试参数中提取 login,这样可以更好地识别测试。

  • test_user_fixture 返回的是一个 (login, password) 的元组,所以在测试中我们可以把它拆开,这样更方便:

      login, password = test_user_fixture
    
  • 我更喜欢用 logging 而不是 print,因为我可以通过在 pyproject.toml 中这一行来控制它的开关:

      log_cli="true"
    

    只需把 true 替换成 false,就可以有效地关闭所有日志记录。我还可以通过这一行来控制日志级别(比如 WARN、INFO、DEBUG 等):

      log_level="DEBUG"
    

更新

如果你去掉 ids= 的部分,输出会变成这样:

test_login_page.py::TestPositiveLogin::test_positive_login[test_user_fixture0] PASSED
test_login_page.py::TestPositiveLogin::test_positive_login[test_user_fixture1] PASSED

注意方括号里面的 test_user_fixture0test_user_fixture1:它们是 pytest 自动生成的 ID,并没有什么用。

我希望在这些方括号里放的是有用的 ID,比如 login 名称。

根据 pytest 文档ids= 可以是一个 ID 的序列。这意味着以下的写法是一样的:

USERS_DATA = list(user_data_generator())
TEST_IDS = [user_data[0] for user_data in USERS_DATA]
@pytest.fixture(
    scope="session",
    autouse=True,
    params=USERS_DATA,
    ids=TEST_IDS,
)
def test_user_fixture(request):
    ...

例如,如果 USERS_DATA

[
    ("user1", "password1"),
    ("user2", "password2"),
]

那么 TEST_IDS 将会是

[
    "user1",
    "user2",
]

这些 ID 将会在方括号里面使用。注意 ids= 也可以是一个函数,这个函数接收 params= 参数中的单个元素并返回一个 ID。在这种情况下,我们有:

USERS_DATA = list(user_data_generator())
def test_id(user_data):
    # Example of user_data: ("user1", "password1")
    return user_data[0]

@pytest.fixture(
    scope="session",
    autouse=True,
    params=USERS_DATA,
    ids=test_id,
)
def test_user_fixture(request):
    ...

这意味着 pytest 会把 USERS_DATA 中的每个元素传入 test_id 函数,并使用返回值作为 ID。

我们应该使用哪种方法呢?我认为第一种方法使用 TEST_IDS 更容易理解。第二种方法更强大,这也是我在项目中使用的方法。

撰写回答