Search code examples
pytestfixtures

pass a parameter to a pytest fixture for teardown


The problem: Clean up test artifacts created from the test. In the case below how can a single row created during a test be deleted from the database using a pytest fixture? (Not everything should be deleted from the table after every run. Otherwise a Delete all rows or drop table could be used). The row identifier of the created row is saved in a function variable during the test.

Is it possible to pass a variable created during the test as a parameter into a fixture in pytest? The fixture needs to always run whether the test completed either by it failing or succeeding. The row identifier won’t be known until the test is ran.

Problem illustrated with a fixture

@pytest.fixture()
def clean_up_db_row(row_id):
    yield
    delete_from_db(self.row_id). # code to delete the row based on the id


def test_something_added_to_database(clean_up_db_row):
    row_id = create_db_row()  # function under test
    ...
    assert row_id in db  # test that information added to the database

    # the clean_up_db_row fixture will always run but how will it know about the id variable defined in the function?

If an assert fails halfway through a test the row added during the test doesn’t get deleted when tacking the clean up to the end. Because the test stop executing.

Example of the problem is with out a pytest fixture:

def clean_up_db_row(row_id):
    yield
    delete_from_db(row_id). # code to delete the row based on the id


def test_something_added_to_database():
    row_id = create_db_row()  # function under test
    ...
    assert row_id in db  # test that information added to the database
    clean_up_db_row(row_id)  # this won’t run if there is a failure

Solution without a pytest fixture


def clean_up_db_row(row_id):
    yield
    delete_from_db(row_id). # code to delete the row based on the id

def test_something_added_to_database():
    row_id = create_db_row()  # function under test
    ...
    try:
        assert row_id in db  # test that information added to the database
    except Exception as e:
        raise e
    finally:
        clean_up_db_row(row_id)  # this will always run but doesn’t use a fixture

Potential solution using an instance variable on a class

class TestCaseCleanUp:

    @pytest.fixture(autouse=True)
    def clean_up_db_row(self):
        yield
        delete_from_db(self.row_id). # code to delete the row based on the id


    def test_something_added_to_database(self):
        self.row_id = create_db_row()  # function under test
        ...
        assert self.row_id in db  # test that information added to the database
        # the autouse fixture can use the self.row_id assigned

Solution

  • The object yielded by a fixture is injected into the test context by the runner. This can be any object you want, and the test is free to mutate it.

    Here is a pattern that should work for you:

    @pytest.fixture()
    def clean_up_db_rows():
        row_ids = []
        yield row_ids
        for row_id in row_ids:
            delete_from_db(row_id)
    
    
    def test_something_added_to_database(clean_up_db_rows):
        row_id = create_db_row()  # function under test
        clean_up_db_rows.append(row_id)
        ...
        assert row_id in db  # test that information added to the database