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
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
I assume that USERS_DATA contains [(user, password, ...), ...]
The params=USERS_DATA
parametrize the fixture. Even though this fixture is scoped at session level, it will be called once for each element in USERS_DATA
. In other word, if USERS_DATA
contains 2 elements, this fixture will be called twice.
The test_id()
function extracts the login
from the test parameter, thus provide a better way to identify the tests.
The test_user_fixture
returns a tuple of (login, password)
, so in the test, we unpack it to make it easier:
login, password = test_user_fixture
I prefer to use logging
over print
because I can turn on/off via this line in pyproject.toml
:
log_cli="true"
just replace true
with false
and I can effectively turn off all logging. I can also control the log level (e.g. WARN, INFO, DEBUG, ...) with this line:
log_level="DEBUG"
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.