pythonpytestfixturesparametrized-testing

How can I provide fixture yielded data to parametrization of a test function? If I can't, is there any alternatives?


I need to create multiple users (by sending HTTP requests) and run login page tests with created users data (login, password)

I have fixture, that generates users and provides their login data (list of tuples like [(login1, password1), (login2, password2)] in yield. I want to use this yield data as parametrization, because I know only one correct way to run 1 test multiple times with different test data.

Here's code:

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)

Here I tried just using fuxture as parameters, because login_data perfectly fits into parameters data format, but python says

NameError: name 'test_user_fixture' is not defined

Can you please help me solving this problem or maybe give another solution


Solution

  • Your current test_user_fixture fixture returns a list of login/password. I am sure you are using yield because you want some clean up later (e.g. remove the users).

    My proposal is for test_user_fixture to return just a single login/password. Then we can parametrize this fixture using the params= keyword:

    # 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")
    

    and

    # 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)
    

    and

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

    Output when log_cli="false":

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

    Output when 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'
    

    Notes

    Update

    If you remove the ids= part, then the output will look like this:

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

    Notice the part inside the square brackets test_user_fixture0 and test_user_fixture1: they are IDs which pytest generate automatically and they are not helpful.

    What I want to place inside these square brackets are IDs which are useful such as the login name.

    According to pytest doc, the ids= could be a sequence of IDs. That means the following works the same way:

    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):
        ...
    

    For example, if USERS_DATA is

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

    Then, TEST_IDS will be

    [
        "user1",
        "user2",
    ]
    

    And these IDs will be used inside of the square brackets. Note that the ids= can also be a function which take in a single element of the params= parameter and return an ID. In this case we have:

    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):
        ...
    

    That means pytest will pass each element in USERS_DATA into the function test_id and use the return value as the ID.

    Which method should we use? I believe the first method with TEST_IDS are easier to understand. The second method is more powerful and that is what I use in my projects.