Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're a professional chef. Before you start cooking a signature dish—say, a Beef Wellington—you don't just start from scratch the moment the order hits the kitchen. You have a "mise en place." Your onions are already diced, your butter is softened, and your pans are preheated. You've prepared the environment so that the actual act of cooking can be focused and efficient. If you had to chop every single carrot every time you wanted to test a new seasoning, you'd spend more time prepping than actually refining the flavor.

In Pytest, a fixture is exactly that: your mise en place. It's a way to set up the state, data, or connections your tests need before the actual test logic runs, and optionally, clean them up afterward. I've seen too many developers clutter their test functions with ten lines of setup code before they even get to the assert statement. It makes the tests hard to read and a nightmare to maintain. Fixtures pull that noise out of the way.

Stop Repeating Your Setup Logic

Let's say we're building a system that manages user profiles in a database. To test if a user's email can be updated, you first need a user to exist in the database. Without fixtures, you'd be writing the "create user" code in every single test function. It's tedious.

Instead, we define a fixture. You just decorate a function with @pytest.fixture, and then you "inject" that fixture into any test that needs it by adding it as an argument.

import pytest

@pytest.fixture
def mock_user():
    # This is our 'mise en place'
    return {
        "id": 42, 
        "username": "coding_wizard", 
        "email": "wizard@example.com", 
        "is_active": True
    }

def test_user_email_update(mock_user):
    # The mock_user dict is passed in automatically by pytest
    mock_user["email"] = "new_email@example.com"
    assert mock_user["email"] == "new_email@example.com"

def test_user_is_active(mock_user):
    assert mock_user["is_active"] is True

Notice how test_user_email_update doesn't call mock_user()? Pytest sees the argument name, finds the fixture with the matching name, runs it, and hands the result to your test. It's a clean separation of concerns.

Handling the Cleanup with Yield

Sometimes, preparing the environment isn't enough; you also have to clean up. If you open a database connection or create a temporary file, you can't just leave those hanging around, or you'll eventually crash your test suite or pollute your filesystem. This is where the yield keyword comes in.

When you use yield instead of return, Pytest runs everything before the yield, executes your test, and then comes back to run everything after the yield. I like to think of it as the "cleaning the station" phase of the kitchen analogy.

import pytest

class DatabaseConnection:
    def connect(self): print("\nConnecting to DB...")
    def close(self): print("Closing DB connection...")

@pytest.fixture
def db_session():
    db = DatabaseConnection()
    db.connect()
    
    yield db  # The test happens right here
    
    db.close() # This runs after the test is finished

def test_database_query(db_session):
    print("Running query...")
    assert db_session is not None

If the test fails, the code after the yield still runs. This is critical. You don't want a failing test to leave a database lock open that causes the next fifty tests to fail for completely unrelated reasons.

Controlling How Often Setup Happens

By default, a fixture runs once for every single test function that requests it. This is usually what you want because it ensures a "clean slate." However, if your setup is expensive—like spinning up a Docker container or migrating a huge database—doing that for 100 tests will make your suite crawl.

You can change the "scope" of a fixture. For example, scope="module"` tells Pytest to run the fixture once per Python file, and scope="session"` runs it once for the entire test run. Use this sparingly; the more you share state between tests, the higher the risk that one test will accidentally break another.

@pytest.fixture(scope="session")
def global_api_config():
    # This only runs once, no matter how many tests use it
    return {"api_key": "secret_123", "timeout": 30}



📋 Practical Task

Exercise: Building a Mock API Client Session

You are testing a service that interacts with a remote API. To prevent your tests from making actual network calls, you need a fixture that simulates an API client session. Your goal is to create a fixture that handles both the initialization and the teardown of this session.

Requirements:

  • Create a class called MockApiClient with two methods: connect() (which should print "API Session Started") and disconnect() (which should print "API Session Closed").
  • Create a pytest fixture named api_client.
  • The fixture must use yield to ensure that connect() is called before the test and disconnect() is called after the test.
  • Write a test function called test_api_connection_exists that uses the api_client fixture and asserts that the client is not None.

Verify your solution by running the test; you should see both the "Started" and "Closed" print statements in the output.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.