← Back to Chapters

Python Testing with pytest

? Python Testing with pytest

⚡ Quick Overview

pytest is a popular Python testing framework that makes it simple, readable, and powerful to write and run automated tests. It supports simple unit tests, complex functional tests, fixtures, parametrization, and rich plugins & reporting.

? Key Concepts

  • Test discovery — pytest automatically finds files named test_*.py or *_test.py.
  • Simple asserts — you just use the built-in assert keyword; pytest gives detailed failure messages.
  • Fixtures — reusable setup/teardown logic shared by multiple tests.
  • Parametrization — run the same test with many different data inputs.
  • Markers — tag tests as slow, smoke, db, etc., and selectively run them.
  • Plugins — extend pytest with coverage, HTML reports, Django/Flask support, and more.

? Syntax and Theory

? Test File and Function Naming

  • Test files: test_something.py or something_test.py.
  • Test functions: names must start with test_.
  • Run all tests: in the project folder, run pytest from the terminal.

? Arrange-Act-Assert Pattern

Most pytest tests follow the AAA pattern:

  • Arrange — set up data, objects, and preconditions.
  • Act — call the function or method under test.
  • Assert — check the result with assert statements.

? Fixtures Overview

Fixtures are functions marked with @pytest.fixture that return reusable objects. Tests request fixtures just by listing them as function parameters. pytest handles creation, reuse, and cleanup.

? Code Examples

? Basic Test File with pytest
# math_utils.py
def add(a, b):
    return a + b

def divide(a, b):
    return a / b

# test_math_utils.py
import math_utils

def test_add():
    # Arrange & Act
    result = math_utils.add(2, 3)
    # Assert
    assert result == 5

def test_divide():
    result = math_utils.divide(10, 2)
    assert result == 5
? Using Fixtures for Reusable Setup
# conftest.py (shared fixtures)
import pytest

@pytest.fixture
def sample_numbers():
    print("Setting up sample_numbers fixture")
    nums = [1, 2, 3, 4, 5]
    yield nums
    print("Tearing down sample_numbers fixture")

# test_with_fixture.py
def test_sum(sample_numbers):
    total = sum(sample_numbers)
    assert total == 15

def test_length(sample_numbers):
    assert len(sample_numbers) == 5
? Parametrized Tests
# Example of parametrized tests with pytest
import pytest

def is_even(n: int) -> bool:
    return n % 2 == 0

# Each pair is (input_value, expected_result)
@pytest.mark.parametrize(
    "value, expected",
    [
        (2, True),
        (3, False),
        (10, True),
        (15, False),
    ],
)
def test_is_even(value, expected):
    # Assert that is_even returns the expected result
    assert is_even(value) == expected
? Testing Exceptions with pytest.raises
import pytest

# Basic division function
def divide(a, b):
    return a / b

# Verify that dividing by zero raises ZeroDivisionError
def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)
?️ Using Markers to Group Tests
import pytest

@pytest.mark.slow
def test_heavy_computation():
    total = 0
    for i in range(10_000_000):
        total += i
    assert total > 0

@pytest.mark.api
def test_api_call():
    # imagine calling an external API here
    response_status = 200
    assert response_status == 200

# Run only slow tests:
#   pytest -m slow
#
# Run tests marked "api" but not "slow":
#   pytest -m "api and not slow"

? Live Output and Explanation

?️ Running pytest from the Command Line

Once your test files are ready, open a terminal in your project directory and run pytest. pytest will automatically discover and execute tests.

$ pytest
============================= test session starts =============================
collected 4 items

test_math_utils.py ..                                              [ 50%]
test_with_fixture.py ..                                            [100%]

============================== 4 passed in 0.03s ==============================

Each dot (.) represents a passing test. If a test fails, pytest shows a F plus a detailed traceback and a helpful comparison of expected vs actual values.

? Tips and Best Practices

  • Keep test files close to the code they test (for example, in the same folder).
  • Use descriptive test names like test_user_login_with_valid_credentials.
  • Prefer many small, focused tests over a few large, complex tests.
  • Use fixtures for setup that appears in more than one test.
  • Use parametrization to avoid copy-pasting similar tests with different data.
  • Run tests frequently (on every save or commit) to catch regressions early.
  • Integrate pytest into CI/CD pipelines for automated testing on every push.

? Try It Yourself

  1. Create a file calculator.py with functions: add, subtract, multiply, divide. Then write a corresponding test_calculator.py using pytest.
  2. Add a fixture that returns a preconfigured calculator object or default numbers. Use that fixture in at least two tests.
  3. Write a parametrized test for your add function that checks at least five different input combinations.
  4. Add a test that verifies divide raises ZeroDivisionError when the denominator is zero (use pytest.raises).
  5. Create a custom marker @pytest.mark.integration on one test and practice running only those tests with pytest -m integration.

? When to Use pytest

  • For unit testing pure Python functions and classes in any type of project.
  • For integration testing APIs, databases, and web applications.
  • For regression testing in long-lived projects with frequent changes.
  • Whenever you want powerful assertions, fixtures, and plugins with minimal boilerplate.