๐ŸŒ Detecting your locationโ€ฆ
๐Ÿ“ข Advertisement โ€” Configure AdSense in Appearance โ†’ Customize โ†’ AdSense Settings

How to Write Unit Tests in Python with pytest: Complete 2026 Guide

โฑ๏ธ5 min read  ยท  925 words

pytest is the de facto standard for testing Python code in 2026 โ€” simpler and more powerful than the built-in unittest. Good tests catch bugs before production, enable confident refactoring, and document how your code should behave. This guide takes you from your first test to advanced patterns.

Setup

pip install pytest pytest-cov

# Project structure
myproject/
โ”œโ”€โ”€ src/
โ”‚   โ””โ”€โ”€ calculator.py
โ”œโ”€โ”€ tests/
โ”‚   โ””โ”€โ”€ test_calculator.py
โ””โ”€โ”€ pytest.ini

Your First Test

# src/calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# tests/test_calculator.py
from src.calculator import add, divide
import pytest

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

def test_divide():
    assert divide(10, 2) == 5

def test_divide_by_zero_raises():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)
# Run the tests
pytest                    # run all tests
pytest -v                 # verbose (shows each test name)
pytest tests/test_calculator.py::test_add   # run one test
pytest -k "divide"        # run tests matching "divide"

The AAA Pattern (Arrange, Act, Assert)

def test_user_discount():
    # Arrange โ€” set up test data
    user = User(name="Alice", is_premium=True)
    cart = Cart(total=100)

    # Act โ€” call the code under test
    final_price = apply_discount(cart, user)

    # Assert โ€” verify the result
    assert final_price == 80  # 20% premium discount

Fixtures โ€” Reusable Test Setup

import pytest

@pytest.fixture
def sample_user():
    # Provides a fresh user for each test that needs one
    return User(name="Alice", email="alice@example.com")

@pytest.fixture
def db_connection():
    # Setup and teardown - code after yield runs as cleanup
    conn = create_test_db()
    yield conn          # test runs here
    conn.close()        # cleanup after test

# Tests receive fixtures as arguments
def test_user_email(sample_user):
    assert sample_user.email == "alice@example.com"

def test_save_user(sample_user, db_connection):
    db_connection.save(sample_user)
    assert db_connection.count() == 1

Parametrization โ€” Test Many Cases

import pytest

@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_add_many_cases(a, b, expected):
    assert add(a, b) == expected

# Runs 4 separate tests, one per tuple โ€” clear failure reporting per case

Mocking โ€” Isolate Code from Dependencies

from unittest.mock import patch, MagicMock

# Mock an external API call so tests don't hit the network
def test_fetch_user_data():
    with patch('src.api.requests.get') as mock_get:
        mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
        mock_get.return_value.status_code = 200

        result = fetch_user_data(1)

        assert result["name"] == "Alice"
        mock_get.assert_called_once_with("https://api.example.com/users/1")

# Mock with pytest-mock (cleaner)
def test_with_mocker(mocker):
    mock_db = mocker.patch('src.service.database')
    mock_db.get_user.return_value = {"name": "Bob"}

    result = get_user_name(1)
    assert result == "Bob"

Testing Exceptions and Edge Cases

def test_raises_on_invalid_input():
    with pytest.raises(ValueError) as exc_info:
        validate_age(-5)
    assert "must be positive" in str(exc_info.value)

def test_edge_cases():
    assert process([]) == []           # empty input
    assert process([1]) == [1]         # single item
    assert process(None) is None       # None handling

Measuring Coverage

# Run tests with coverage report
pytest --cov=src --cov-report=term-missing

# Output shows which lines aren't tested:
# Name                 Stmts   Miss  Cover   Missing
# src/calculator.py       10      1    90%    15

# Generate an HTML report
pytest --cov=src --cov-report=html
# Open htmlcov/index.html to see line-by-line coverage

Configuration (pytest.ini)

# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
addopts = -v --cov=src --cov-report=term-missing
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    integration: integration tests

Best Practices

  • One assertion concept per test: Each test verifies one behavior โ€” failures point directly to the problem
  • Descriptive names: test_divide_by_zero_raises_valueerror tells you what broke without reading the code
  • Test behavior, not implementation: Test what the function does, not how โ€” so refactoring doesn’t break tests
  • Keep tests fast and isolated: Each test should run independently in any order
  • Mock external dependencies: Databases, APIs, and file systems should be mocked in unit tests
  • Aim for meaningful coverage: 80%+ is a good target, but cover critical paths thoroughly rather than chasing 100%

Frequently Asked Questions

Q: pytest vs unittest?
A: pytest is simpler (plain assert, no boilerplate classes), more powerful (fixtures, parametrization), and has a rich plugin ecosystem. It can run unittest tests too. Use pytest for new projects.

Q: How much test coverage do I need?
A: 80% is a common target. But coverage is a guide, not a goal โ€” 100% coverage with weak assertions is worse than 80% with thorough tests of critical logic. Focus on covering the code that matters.

Q: Should I mock the database or use a test database?
A: Mock it in unit tests (fast, isolated). Use a real test database in integration tests (verifies actual queries work). Both have their place โ€” unit tests for logic, integration tests for the data layer.

Q: What’s the difference between a fixture and a regular function?
A: Fixtures are managed by pytest โ€” they’re set up before tests that request them and can clean up after (via yield). They provide reusable, isolated test dependencies with automatic lifecycle management.

Q: How do I test async code?
A: Install pytest-asyncio and mark tests with @pytest.mark.asyncio. Then you can await inside your test functions to test async functions.

Conclusion

pytest makes Python testing simple and powerful. Start with plain assert statements in the AAA pattern, use fixtures for reusable setup, parametrize to cover many cases concisely, and mock external dependencies to keep tests fast and isolated. Measure coverage to find untested code, but prioritize testing critical logic thoroughly over chasing 100%. Good tests are an investment that pays off every time you refactor or add features with confidence.

โœ๏ธ Leave a Comment

Your email address will not be published. Required fields are marked *

๐ŸŒ Read in:๐Ÿ‡ฌ๐Ÿ‡ง English๐Ÿ‡ฉ๐Ÿ‡ช Deutsch๐Ÿ‡ง๐Ÿ‡ท Portuguรชs๐Ÿ‡ธ๐Ÿ‡ฆ ุงู„ุนุฑุจูŠุฉ๐Ÿ‡ฎ๐Ÿ‡ณ เคนเคฟเคจเฅเคฆเฅ€๐Ÿ‡ง๐Ÿ‡ฉ เฆฌเฆพเฆ‚เฆฒเฆพ