KAHIBARO
Discord Login Register

19.5. pytest

Why pytest?

pytest is a popular Python testing framework that makes tests:

You will use pytest in almost every modern Python backend project, especially with FastAPI and database code.

Key ideas:

Rule: In this course, always use pytest as the main test runner for Python backend projects.

In this chapter you will learn how to:

Installing pytest

Use pip to install pytest into your virtual environment.

bash
pip install pytest

Check that pytest is installed:

bash
pytest --version

You should see something like:

text
pytest 8.3.0

In many real projects, pytest will be added to your requirements.txt or pyproject.toml under development dependencies.

Example requirements.txt snippet:

text
fastapi
uvicorn
sqlalchemy
pytest
pytest-asyncio

Later, when someone runs:

bash
pip install -r requirements.txt

pytest will be installed as well.

How pytest discovers tests

pytest looks for tests using a simple naming convention.

By default, pytest:

So:

File nameFunction nameWill pytest run it?
test_math.pytest_addYes
math_test.pytest_addYes
math.pytest_addNo (file name wrong)
test_math.pyaddNo (function name wrong)

Example structure:

text
project/
    app/
        main.py
    tests/
        test_example.py

tests/test_example.py:

python
def test_example():
    assert 1 + 1 == 2

Run:

bash
pytest

pytest will find tests/test_example.py, then find test_example and run it.

You usually do not have to list each test file separately. Just run pytest at the project root and let it auto-discover.

Running tests with pytest

The most common commands:

bash
# Run all tests
pytest
# Run tests with more verbose output
pytest -v
# Run tests in a specific file
pytest tests/test_math.py
# Run a specific test function
pytest tests/test_math.py::test_add
# Show print() output
pytest -s
# Stop after first failure
pytest -x

Example:

bash
pytest -v tests/test_math.py

Output (simplified):

text
==================== test session starts ====================
collected 2 items
tests/test_math.py::test_add PASSED                     [ 50%]
tests/test_math.py::test_subtract PASSED                [100%]
==================== 2 passed in 0.02s ======================

Use this pattern often when developing:

  1. Edit code or tests.
  2. Run pytest -q or pytest -q tests/test_something.py to get quick feedback.

Writing your first pytest tests

A simple test

Create tests/test_math.py:

python
def add(a, b):
    return a + b
def test_add_positive_numbers():
    result = add(2, 3)
    assert result == 5

Run:

bash
pytest -v tests/test_math.py

Key points:

pytest will run the test and interpret any failing assert as a test failure.

Multiple tests in a file

python
def add(a, b):
    return a + b
def subtract(a, b):
    return a - b
def test_add_positive_numbers():
    assert add(2, 3) == 5
def test_add_negative_numbers():
    assert add(-2, -3) == -5
def test_subtract_result_positive():
    assert subtract(5, 3) == 2
def test_subtract_result_negative():
    assert subtract(3, 5) == -2

Each test_* function is a separate test. If one fails, the others can still pass.

Assertions in pytest

Basic assertions

pytest uses the built-in Python assert keyword, but it gives better error messages.

python
def test_assertion_examples():
    # equality
    assert 1 + 1 == 2
    # inequality
    assert 3 != 4
    # truthy / falsy
    assert "backend"
    assert not ""
    # membership
    assert "a" in "backend"
    assert 2 in [1, 2, 3]
    # type checking
    value = 10
    assert isinstance(value, int)

If an assertion fails, pytest shows why.

python
def test_fail():
    assert 1 + 1 == 3

Output:

text
>       assert 1 + 1 == 3
E       assert 2 == 3
E        +  where 2 = (1 + 1)

This detail helps you debug quickly.

Rule: In pytest tests, always use simple assert statements instead of unittest style methods like self.assertEqual.

Checking exceptions

Sometimes you expect your code to raise an error.

Use pytest.raises as a context manager:

python
import pytest
def divide(a, b):
    if b == 0:
        raise ValueError("division by zero")
    return a / b
def test_divide_by_zero():
    with pytest.raises(ValueError) as exc_info:
        divide(10, 0)
    assert "division by zero" in str(exc_info.value)

This checks that:

Parametrized tests

Often you want to test the same function with many input/output pairs. Instead of writing many separate test functions, you can parametrize a single test.

Example without parametrization:

python
def double(x):
    return x * 2
def test_double_1():
    assert double(1) == 2
def test_double_2():
    assert double(2) == 4
def test_double_3():
    assert double(3) == 6

With parametrization:

python
import pytest
def double(x):
    return x * 2
@pytest.mark.parametrize(
    "input_value, expected",
    [
        (1, 2),
        (2, 4),
        (3, 6),
        (-1, -2),
    ],
)
def test_double(input_value, expected):
    assert double(input_value) == expected

pytest will run test_double four times, with different arguments.

This is very useful in backend tests for:

Example for HTTP-like logic:

python
import pytest
def is_valid_status(code: int) -> bool:
    return 200 <= code < 300
@pytest.mark.parametrize(
    "code, expected",
    [
        (200, True),
        (201, True),
        (204, True),
        (301, False),
        (400, False),
        (500, False),
    ],
)
def test_is_valid_status(code, expected):
    assert is_valid_status(code) == expected

Using fixtures (basic idea)

Fixtures are special functions that provide reusable data or setup for tests, such as:

Here is a very simple example:

python
import pytest
def connect_to_fake_db():
    return {"connected": True}
@pytest.fixture
def db():
    # Setup part
    connection = connect_to_fake_db()
    yield connection
    # Teardown part could go here (for example close connection)
def test_db_connection(db):
    assert db["connected"] is True

What happens:

  1. pytest sees @pytest.fixture on db, so db becomes a fixture.
  2. The test_db_connection function has a parameter db.
  3. pytest calls the db fixture before the test, gets the value from yield, and passes it into the test function.

In later chapters you will see more realistic fixtures, for example:

For now, remember:

Rule: If a test function has a parameter name that matches a fixture, pytest automatically provides that fixture.

Test structure and naming

Test directory structure

A common layout in backend projects:

text
project/
    app/
        __init__.py
        main.py
        models.py
        services/
            users.py
    tests/
        __init__.py
        test_main.py
        test_users.py
        services/
            test_users_service.py

Example mapping:

Code fileTest file
app/main.pytests/test_main.py
app/services/users.pytests/services/test_users.py
app/models.pytests/test_models.py

This makes it easy to find tests for a given piece of code.

Naming tests

Use clear, descriptive names that say what should happen.

Bad:

python
def test_1():
    ...

Better:

python
def test_create_user_stores_hashed_password():
    ...
def test_create_user_rejects_duplicate_email():
    ...

Common patterns:

Examples:

python
def test_register_user_returns_201_on_success():
    ...
def test_login_returns_401_for_invalid_credentials():
    ...

Basic configuration with pytest.ini

You can configure pytest behavior with a pytest.ini file in the project root.

Simple example:

ini
# pytest.ini
[pytest]
minversion = 8.0
addopts = -ra -q
testpaths =
    tests

Explanation:

In real backend projects you might also add markers or asyncio settings here, but that belongs in later chapters.

Using pytest for backend-style code

Here is a small, backend-flavored example to connect everything.

Imagine you have a simple authentication service:

app/auth.py:

python
from hashlib import sha256
def hash_password(password: str) -> str:
    return sha256(password.encode("utf-8")).hexdigest()
def check_password(password: str, password_hash: str) -> bool:
    return hash_password(password) == password_hash

You write tests like this:

tests/test_auth.py:

python
from app.auth import hash_password, check_password
def test_hash_password_returns_same_length_for_same_algorithm():
    hash1 = hash_password("secret1")
    hash2 = hash_password("secret2")
    assert len(hash1) == len(hash2)
def test_check_password_returns_true_for_correct_password():
    password = "mysecret"
    password_hash = hash_password(password)
    assert check_password("mysecret", password_hash) is True
def test_check_password_returns_false_for_wrong_password():
    password_hash = hash_password("correct")
    assert check_password("wrong", password_hash) is False

Run:

bash
pytest -v

You will see three tests executed. This is the same pattern you will use later when testing:

Summary

In this chapter you learned the basics of pytest:

You will apply these foundations in later chapters when testing FastAPI apps, databases, and authentication logic.

Views: 6

Comments

Please login to add a comment.

Don't have an account? Register now!