Python API Development with FastAPI

Testing

Conftest Py File

In this article, we explore how to organize test fixtures using a dedicated file called conftest.py. Pytest automatically discovers and loads fixtures defined in this file for all tests within the same package (including sub-packages). This centralized approach simplifies database and client initialization, allowing you to reuse common setup logic across multiple test modules.


Testing the Login Endpoint

Below is a sample test that logs in a user. This test uses the client fixture (provided via conftest.py) to send a POST request to the /login endpoint. The returned JWT token is decoded to verify the user ID and token type.

def test_login_user(test_user, client):
    res = client.post(
        "/login", data={"username": test_user['email'], "password": test_user['password']}
    )
    login_res = schemas.Token(**res.json())
    payload = jwt.decode(login_res.access_token, settings.secret_key, algorithms=[settings.algorithm])

    user_id = payload.get('user_id')
    assert user_id == test_user['id']
    assert login_res.token_type == "bearer"
    assert res.status_code == 200

The following console output shows the results when running the tests, along with deprecation warnings related to aiofiles:

venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\fastapi\venv\lib\site-packages\aiofiles\os.py:10: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
============================= test session starts ==============================
collected 2 items

tests/test_users.py::test_login_user PASSED                            [ 50%]
tests/test_calculations.py::test_calculations PASSED                  [100%]

============================== 2 passed, 5 warnings in 1.67s ==============================

Note

The conftest.py file makes its fixtures available to all tests in the same package, eliminating the need for repetitive import statements.


Shared Fixtures in Conftest.py

By moving all database-related logic and fixtures to conftest.py, every test in the package can seamlessly use them without extra import statements. For example, consider the following segments of console output, which confirm that tests using shared fixtures like session and client are executing within a consistent test environment:

venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
C:\users\sanje\documents\courses\fastapi\venv\lib\site-packages\aiofiles\os.py:10: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
-- Docs: https://docs.pytest.org/en/stable/warnings.html
===================== 2 passed, 5 warnings in 1.67s =====================

Custom Client Fixture

The client fixture further demonstrates how to override the default database dependency by providing a custom session. This ensures that any test relying on database interactions automatically uses this setup.

@pytest.fixture()
def client(session):
    def override_get_db():
        try:
            yield session
        finally:
            session.close()
    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)

When running tests that exercise the client fixture, you might see output such as:

venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
C:\users\sanje\documents\courses\fastapi\venv\lib\site-packages\aiofiles\os.py:10: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
=== 2 passed, 5 warnings in 1.67s ===

Warning

Ensure that you update deprecated decorators (such as @coroutine) in your dependencies to avoid future compatibility issues, particularly if upgrading Python versions.


Creating a Test User Fixture

Another common scenario is creating a test user. By defining the test user fixture in conftest.py, you can share it across multiple test modules (e.g., user, voting, posts tests), eliminating redundancy.

@pytest.fixture
def test_user(client):
    user_data = {"email": "[email protected]", "password": "password123"}
    res = client.post("/users/", json=user_data)
    assert res.status_code == 201

    new_user = res.json()
    new_user['password'] = user_data['password']
    return new_user

The console output for tests that depend on this fixture may look as follows:

venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
C:\users\sanje\documents\courses\fastapi\venv\lib\site-packages\aiofiles\os.py:10: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
===================== test session starts =====================
...
2 passed, 5 warnings in 1.67s

Example Test File Usage

In your test files (e.g., test_users.py), you do not need to import the client or test_user fixtures explicitly. They are automatically available thanks to conftest.py. The following example illustrates how to use these fixtures:

def test_create_user(client):
    res = client.post(
        "/users/", json={"email": "[email protected]", "password": "password123"}
    )
    new_user = schemas.UserOut(**res.json())
    assert new_user.email == "[email protected]"
    assert res.status_code == 201

def test_login_user(test_user, client):
    res = client.post(
        "/login", data={"username": test_user['email'], "password": test_user['password']}
    )
    login_res = schemas.Token(**res.json())
    payload = jwt.decode(login_res.access_token, settings.secret_key, algorithms=[settings.algorithm])

    user_id = payload.get('user_id')
    assert user_id == test_user['id']
    assert login_res.token_type == "bearer"
    assert res.status_code == 200

The corresponding console output confirms that everything is functioning correctly:

venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
venv\lib\site-packages\aiofiles\os.py:10
C:\users\sanje\documents\courses\fastapi\venv\lib\site-packages\aiofiles\os.py:10: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
======================= warnings summary =======================
2 passed, 5 warnings in 1.67s

Modular Fixture Management

For modular testing, you can define package-specific fixtures by including a conftest.py file within a given package scope. Tests within that package will use its fixtures, while tests outside will use the top-level conftest.py. For instance, consider the following snippet from a test file in a different package:

def test_create_user(client):
    res = client.post("/users/", json={"email": "[email protected]", "password": "password123"})

This design allows for flexible and modular fixture management. Even if multiple conftest.py files exist throughout the project, each one scopes its fixtures to its directory and subdirectories, ensuring that tests only have access to the fixtures they require.


Conclusion

Centralizing fixtures in conftest.py simplifies test organization, reduces redundancy, and enhances maintainability across the testing suite. By properly leveraging shared fixtures such as client and test_user, you can improve test consistency and streamline your testing process.

For more information on testing with Pytest, refer to the Pytest Documentation.

Happy Testing!

Watch Video

Watch video content

Previous
Validate Token