Question

Choosing Between yield and addfinalizer in pytest Fixtures for Teardown

I've recently started using pytest for testing in Python and created a fixture to manage a collection of items using gRPC. Below is the code snippet for my fixture:

import pytest

@pytest.fixture(scope="session")
def collection():
    grpc_page = GrpcPages().collections

    def create_collection(collection_id=None, **kwargs):
        default_params = {
            "id": collection_id,
            "is_active": True,
            # some other params
        }
        try:
            return grpc_page.create_collection(**{**default_params, **kwargs})
        except Exception as err:
            print(err)
            raise err

    yield create_collection

    def delete_created_collection():
        # Some code to hard and soft delete created data

This is my first attempt at creating a fixture, and I realized that I need a mechanism to delete data created during the fixture's lifecycle.

While exploring options for implementing teardown procedures, I came across yield and addfinalizer. From what I understand, both can be used to define teardown actions in pytest fixtures. However, I'm having trouble finding clear documentation and examples that explain the key differences between these two approaches and when to choose one over the other.

Here are the questions (for fast-forwarding :) ):

  1. What are the primary differences between using yield and addfinalizer in pytest fixtures for handling teardown?
  2. Are there specific scenarios where one is preferred over the other?
 3  69  3
1 Jan 1970

Solution

 4

The main difference is that with addfinalizer you can add as many finalizers as you need, useful in complex teardown scenarios.

If your setup and teardown are straightforward, there's no need to use addfinalizer:

@pytest.fixture
def resource():
    resource = create_resource()
    yield resource
    resource.cleanup()

But if you need e.g. multiple cleanup steps, addfinalizer allows you to do that in a more readible way:

@pytest.fixture
def resource(request):
    resource = create_resource()
    request.addfinalizer(resource.cleanup)
    request.addfinalizer(resource.log)
    ...
    return resource

Simple as that :)

2024-07-08
ciurlaro

Solution

 2

The main difference is not the number of addfinalizer or fixtures, there is no difference at all. You can add as many as you want (or just have more than one operation in on of them)

@pytest.fixture(scope='session', autouse=True)
def fixture_one():
    print('fixture_one setup')
    yield
    print('fixture_one teardown')


@pytest.fixture(scope='session', autouse=True)
def fixture_two():
    print('fixture_two setup')
    yield
    print('fixture_two teardown')


def test_one():
    print('test_one')

Output

example.py::test_one 
fixture_one setup
fixture_two setup
PASSED                                    [100%]test_one
fixture_two teardown
fixture_one teardown

The main difference is if the teardown will run in case of a failure in the setup stage. This is useful if there is need for cleanup even if the setup failed.

Without finalizer the teardown won't run if there was an exception in the setup

@pytest.fixture(scope='session', autouse=True)
def fixture_one():
    print('fixture_one setup')
    raise Exception('Error')
    yield
    print('fixture_one teardown')


def test_one():
    print('test_one')

Output

ERROR                                     [100%]
fixture_one setup

test setup failed
...
E       Exception: Error

example.py:8: Exception

But with finalizer it will

@pytest.fixture(scope='session', autouse=True)
def fixture_one(request):
    def finalizer():
        print('fixture_one teardown')

    request.addfinalizer(finalizer)
    print('fixture_one setup')
    raise Exception('Error')
    yield

Output

ERROR                                     [100%]
fixture_one setup

test setup failed
...
E       Exception: Error

example.py:13: Exception
fixture_one teardown
2024-07-09
Guy