Python API Development with FastAPI

Testing

Test User Fixture

In this lesson, we demonstrate how to decouple the login user test from other tests by ensuring the test does not rely on external states. The solution is to create a fixture that sets up a test user before executing the login test.

Below is the original login test code, which creates a user and then performs a login:

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(client):
    res = client.post(
        "/login", data={"username": "[email protected]", "password": "password123"}
    )
    print(res.json())
    assert res.status_code == 200

When running these tests, you might encounter an error similar to:

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
FAILED tests/test_users.py::test_login_user -> assert 403 == 200

This error occurs because the login test expects a user to exist before running. The underlying issue is the need for a consistent fixture order or scope. Our goal is to centralize the user creation logic in a fixture, avoiding code repetition across multiple tests.

Previously, the following snippet was repeated for creating a user and testing login:

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(client):
    res = client.post(
        "/login", data={"username": "[email protected]", "password": "password123"}
    )
    print(res.json())
    assert res.status_code == 200

with output:

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
def run(*args, loop=None, executor=None, **kwargs):
-- Docs: https://docs.pytest.org/en/stable/warnings.html
FAILED tests/test_users.py::test_login_user - assert 403 == 200

Note

Centralizing the user creation logic in a fixture makes our tests modular and avoids code duplication.

Setting Up the User Fixture

First, remove or comment out any redundant tests at the top of your test file. Then, import pytest along with other required modules:

import pytest
from app import schemas
from database import client, session

Now, define a fixture that posts to the user creation endpoint. This fixture asserts successful user creation and returns the user data, including the password for subsequent login:

@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
    print(res.json())
    new_user = res.json()
    # Include the password in the returned data for later authentication
    new_user["password"] = user_data["password"]
    return new_user

Updating the Login Test

With the test user fixture in place, modify the login test to depend on both the client and the test_user fixture. This ensures that any changes in user creation details automatically propagate to the login test:

def test_login_user(client, test_user):
    res = client.post(
        "/login", data={"username": test_user["email"], "password": test_user["password"]}
    )
    print(res.json())
    assert res.status_code == 200

Dependencies: Session and Client Fixtures

The client fixture depends on a session fixture. An example session fixture (typically defined elsewhere) might look like:

engine = create_engine(SQLALCHEMY_DATABASE_URL)

TestingSessionLocal = sessionmaker(
    autocommit=False, autoflush=False, bind=engine
)

@pytest.fixture(scope="module")
def session():
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

Similarly, the client fixture may override the dependency to use our test database session:

@pytest.fixture()
def client(session):
    def override_get_db():
        try:
            yield session
        finally:
            pass
    # The following setup is an example if you're using FastAPI:
    # app.dependency_overrides[get_db] = override_get_db
    # yield TestClient(app)

Example Test Output

When running the tests, you might initially see output similar to:

cachedir: .pytest_cache
rootdir: C:\Users\sanjeev\Documents\Courses\fastapi
plugins: cov-2.12.1
collected 2 items

tests/test_users.py::test_create_user PASSED
tests/test_users.py::test_login_user {'id': 1, 'email': '[email protected]', 'created_at': '2021-09-12T22:33:05.149462-04:00'}
{'detail': 'Invalid Credentials'}
FAILED

After properly linking the fixture and ensuring that the login test sends the correct credentials from the test user, the final output should be:

cachedir: .pytest_cache
rootdir: C:\Users\sanjeev\Documents\Courses\fastapi
plugins: cov-2.12.1
collected 2 items

tests/test_users.py::test_create_user PASSED
tests/test_users.py::test_login_user PASSED

Testing Best Practices

By refactoring our tests in this way, any changes to user credentials in the fixture will automatically reflect in the login test. This modular approach improves the maintainability and reliability of your test suite.

Happy testing!

Watch Video

Watch video content

Previous
Fixture Scope