Testing Your Code with pytest

· 4 min read · Updated March 10, 2026 · intermediate
testing pytest unittest test-automation tdd

Writing tests is a crucial part of building reliable software. pytest is the most popular testing framework in Python. It makes writing tests simple and readable. This guide covers everything you need to write effective tests for your Python code.

Why Use pytest?

pytest discovers tests automatically. You just write functions that start with test_. The framework runs them and reports which ones pass or fail.

pytest is different from unittest. You do not need to extend any classes. You write plain Python functions. This makes tests easier to read and maintain.

# A simple pytest test
def test_addition():
    assert 1 + 1 == 2

Run this with pytest in your terminal.

Installing pytest

Install pytest with pip.

pip install pytest

Verify the installation.

pytest --version

Writing Your First Test

Create a file named test_example.py. pytest finds files that start with test_.

def test_addition():
    result = 2 + 2
    assert result == 4

def test_string():
    text = "hello"
    assert text.upper() == "HELLO"

Run the tests.

pytest test_example.py

pytest finds and runs all functions starting with test_. Each function that runs without raising an AssertionError passes.

Assertions

Assertions are checks that verify your code works. pytest uses Python’s built-in assert statement. When an assertion fails, pytest shows you exactly what went wrong.

def test_divide():
    result = 10 / 2
    assert result == 5

def test_list_operations():
    numbers = [1, 2, 3, 4, 5]
    assert len(numbers) == 5
    assert sum(numbers) == 15
    assert max(numbers) == 5

You can also add custom error messages to assertions.

def test_custom_message():
    x = 1
    y = 2
    assert x == y, f"Expected {x} to equal {y}"

Fixtures

Fixtures provide setup and teardown for tests. They share resources across multiple tests. Use the @pytest.fixture decorator to create fixtures.

import pytest

@pytest.fixture
def database():
    # Setup: create a test database
    db = create_test_database()
    yield db
    # Teardown: clean up after tests
    db.close()

def test_user_creation(database):
    user = database.create_user("alice")
    assert user.name == "alice"

def test_user_count(database):
    count = database.count_users()
    assert count > 0

The yield keyword separates setup from teardown. Code before yield runs before each test. Code after yield runs after each test.

Fixtures can also accept other fixtures as parameters. This creates a dependency chain for complex test scenarios.

@pytest.fixture
def app():
    application = create_app()
    return application

@pytest.fixture
def client(app):
    return app.test_client()

def test_homepage(client):
    response = client.get("/")
    assert response.status_code == 200

Parametrized Tests

Parametrization lets you run the same test with different inputs. Use @pytest.mark.parametrize to avoid duplicating test code.

import pytest

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
    (10, 20),
])
def test_double(input, expected):
    assert input * 2 == expected

@pytest.mark.parametrize("word,uppercase", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("pytest", "PYTEST"),
])
def test_uppercase(word, uppercase):
    assert word.upper() == uppercase

You can also combine multiple parametrizations.

@pytest.mark.parametrize("a,b,result", [
    (1, 1, 2),
    (2, 3, 5),
    (10, -5, 5),
])
def test_addition(a, b, result):
    assert a + b == result

Mocking

Mocking replaces real objects with fake ones during testing. This is useful for testing code that depends on external services, databases, or APIs. Use unittest.mock or the pytest-mock plugin.

from unittest.mock import Mock, patch
import pytest

def test_api_call():
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"data": "test"}

    with patch("requests.get", return_value=mock_response):
        result = fetch_data()
        assert result == {"data": "test"}

The patch decorator temporarily replaces the specified function with a mock. This lets you control what the function returns without making actual HTTP requests.

@pytest.fixture
def mock_database():
    mock = Mock()
    mock.get_user.return_value = {"id": 1, "name": "Alice"}
    return mock

def test_get_user(mock_database):
    user = get_user(1, database=mock_database)
    assert user["name"] == "Alice"
    mock_database.get_user.assert_called_once_with(1)

You can also mock entire modules.

@patch("module.random.randint", return_value=42)
def test_random(mock_rand):
    result = roll_dice()
    assert result == 42

Running Tests

Here are some common pytest commands:

# Run all tests
pytest

# Run a specific file
pytest test_example.py

# Run tests matching a pattern
pytest -k "test_addition"

# Show detailed output
pytest -v

# Stop after first failure
pytest -x

# Show print statements
pytest -s

# Run tests in a specific class
pytest::TestClass

# Generate coverage report
pytest --cov=myapp

Best Practices

Write descriptive test names. The test name should explain what is being tested.

# Good
def test_user_cannot_login_with_wrong_password():
    ...

# Bad
def test_login():
    ...

Keep tests independent. Each test should work on its own without depending on other tests.

Test edge cases. Do not just test the happy path.

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        x = 1 / 0

Use pytest.raises to test that exceptions are raised correctly.

Conclusion

pytest makes testing Python code straightforward. Start with simple tests, then add fixtures, parametrization, and mocking as your test suite grows. Consistent testing leads to reliable, maintainable code.