Categories
Automated testing Pytest Python

How to use pytest fixture with arguments

Pytest fixtures are a very handy tool. Thanks to them, we avoid having to repeat the same code. Also, pytest allows you to control for which tests a fixture should be run.

There is no problem as long as you want the fixture to always give you the same result. But what if each test needs a different result of the fixture? Also, it would be great to manage this result from the test.

When working with functions, this task is solved using arguments that can be passed to functions. And then the function works out based on the arguments that were passed to it.

But when working with fixtures, we don’t call them directly. We simply specify the name of the fixture in the arguments of the test function. Therefore, a special approach has been created for using arguments in fixtures.

Indirect parametrization

This approach is called indirect parametrization and is described in the official pytest documentation here.

So let’s look at a simple example of using indirect parametrization.

Let’s say we’re testing a library’s API. We have 2 tests: one verifies that the query “library.com/users” returns all users, and the second that the query “library.com/books” returns all books. In order to make sure that the queries return everything correctly, we need to get from the database a list of users for the first test and a list of books for the second one.

Basic fixture that we won’t be able to use

Let’s make a fixture that gets data from the database. I simplified the code for working with the database for a better understanding of the meaning.

import pytest

@pytest.fixture(scope='function')
def get_data(table_name):
    db = conntect_to_db()
    query = f'SELECT * FROM {table_name}'
    result = db.execute(query)
    return result

In order to use this fixture, we need the @pytest.mark.parametrize decorator. Typically, this decorator takes the parameter name as the first argument and the parameter value as the second argument as a list. In order to use this decorator for our needs, the name of the fixture must be passed as the first parameter. The second parameter must be passed the parameter which we want to use in this fixture. But this decorator takes a list as its second argument, so we will enclose our parameter in square brackets, which will turn it into a list. Also, we must not forget to indicate that we want to use indirect parametrization. Thus, the line that starts the fixture with the parameter we need will look like this.

@pytest.mark.parametrize('get_data', ['users'], indirect=True)

Final Implementation

So, before starting the test, we will run the “get_data” fixture with the “users” parameter. But the fixture will not be able to work correctly in the form in which it is now created. The problem is that in a fixture, only an argument called “request” can be used for this purpose. No other name can be given to such an argument. Moreover, this argument itself will not store the parameter that you passed, but the object containing this parameter. This means that the parameter still needs to be obtained from the object. This can be done by calling the property param. As a result, after all the necessary changes, the fixture will look like this:

@pytest.fixture(scope='function')
def get_data(request):
    db = connect_to_db()
    query = f'SELECT * FROM {request.param}'
    result = db.execute(query)
    return result

We are now ready to run tests with our fixture. The whole code will look like this:

import pytest

@pytest.fixture(scope='function')
def get_data(request):
    db = connect_to_db()
    query = f'SELECT * FROM {request.param}'
    result = db.execute(query)
    return result

@pytest.mark.parametrize('get_data', ['users'], indirect=True)
def test_users(get_data):
    db_result = get_data
    api_response = get_api_response('users')
    assert api_response == db_result

@pytest.mark.parametrize('get_data', ['books'], indirect=True)
def test_users(get_data):
    db_result = get_data
    api_response = get_api_response('books')
    assert api_response == db_result

Simple example of Indirect Parametrization

In order to be able to see what data the test receives as a result, I created an even simpler example. This is the content of mytest.py file:

import pytest

@pytest.fixture(scope='function')
def get_data(request):
    result = f'My text is {request.param}'
    return result

@pytest.mark.parametrize('get_data', ['1text1'], indirect=True)
def test_one(get_data):
    print(get_data)

@pytest.mark.parametrize('get_data', ['2text2'], indirect=True)
def test_two(get_data):
    print(get_data)

The result of the command “pytest -v -s mytest.py” is:

mytest.py::test_one[1text1] My text is 1text1
PASSED
mytest.py::test_two[2text2] My text is 2text2
PASSED

By Eugene Okulik