pythonpytestfixtures

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?

Solution

  • 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