Testing with pytest

Writing reliable, maintainable Python software requires more than just crafting code that works—it demands verifying that it works as expected over time. One of the most powerful and user-friendly tools to help you achieve this is pytest, a popular testing framework for Python. Whether you're a beginner aiming to build confidence in your code or an aspiring professional wanting to automate quality checks, mastering pytest is essential.

In this lesson, we will explore how to write tests using pytest, run them efficiently, and understand the rich features that make it stand out from other testing tools. By the end, you’ll be equipped to integrate testing seamlessly into your Python projects.

Why Automated Testing?

Imagine you’ve just made a change in your codebase. How do you ensure this change didn’t break existing functionality? Manual testing is tedious and error-prone. Automated tests serve as a safety net, running quickly and consistently to check your code’s behavior.

Testing helps you:

  • Catch bugs early: Detect errors before deployment.
  • Improve code quality: Encourage modular, clear code through writing testable units.
  • Facilitate maintenance: Refactor confidently, knowing tests will alert you to regressions.
  • Document behavior: Tests act as executable specifications.

Introducing pytest

pytest is a testing framework that stands out for its simplicity and powerful features. Unlike Python’s built-in unittest module, pytest requires less boilerplate code and automatically discovers tests, making it friendlier for beginners.

💡 Why pytest?

It supports simple unit tests as well as complex functional testing, integrates with many plugins, and produces clear, readable output. Plus, its assert introspection means you get detailed error messages without extra effort.

Getting Started: Installing pytest

To use pytest, install it via pip. Open your terminal or command prompt and run:

📌 Deep Dive: Installing pytest

PYTHON
pip install pytest

Once installed, you can verify by running:

PYTHON
pytest --version
Output
pytest 7.x.x

Creating Your First Test

pytest automatically discovers test files and functions based on naming conventions. Files should be named test_*.py or *_test.py, and test functions should start with test_.

Let’s write a simple test for a function that adds two numbers.

📌 Deep Dive: Writing a Basic Test

PYTHON
# File: math_ops.py
def add(a, b):
    return a + b


# File: test_math_ops.py
from math_ops import add

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

Here, test_add calls the add function with different inputs and uses assert statements to check the expected output.

Running Tests with pytest

To run tests, navigate to the directory containing your tests and run:

PYTHON
pytest

pytest will automatically find all test files and functions, execute them, and provide a summary:

  • . indicates a passed test.
  • F indicates a failed test.
  • Detailed tracebacks for failures are shown.
PYTHON
============================= test session starts =============================
collected 1 item

test_math_ops.py .                                                        [100%]

============================== 1 passed in 0.02s ==============================

Understanding Assertions and Failures

pytest enhances Python’s built-in assert by introspecting expressions to provide detailed information. For example, if an assertion fails:

📌 Deep Dive: Assertion Failure Details

PYTHON
def test_add_fail():
    assert add(2, 2) == 5
Output
E assert 4 == 5 E + where 4 = add(2, 2)

This clear feedback speeds up debugging tremendously.

Structuring Tests for Readability and Reuse

As your tests grow, organizing them becomes crucial. You can group tests in classes or modules and use fixtures to set up reusable test data or state.

Fixtures are special functions decorated with @pytest.fixture that provide test dependencies and setup/teardown logic.

📌 Deep Dive: Using Fixtures

PYTHON
import pytest

@pytest.fixture
def sample_data():
    return [1, 2, 3, 4]

def test_sum(sample_data):
    assert sum(sample_data) == 10

Here, sample_data is injected automatically into test_sum by pytest, promoting DRY principles.

Running Specific Tests

Sometimes, you may want to run only a subset of tests. pytest makes it easy with command-line options:

  • pytest test_math_ops.py – runs all tests in a file.
  • pytest -k "test_add" – runs tests matching expression (e.g., function name).
  • pytest -m "slow" – runs tests marked with a custom marker (more on this soon).

Marking Tests for Selective Runs

pytest allows marking tests with decorators to categorize or skip them.

Example of marking a test as slow:

PYTHON
@pytest.mark.slow
def test_large_computation():
    # simulate slow test
    pass

Run only slow tests with:

pytest -m slow

⚠️ Important:

When you use custom markers like slow, declare them in your pytest.ini file to avoid warnings:

[pytest]
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')

Handling Expected Failures and Skipping Tests

pytest provides decorators to handle special cases:

  • @pytest.mark.skip(reason="..."): skip a test unconditionally.
  • @pytest.mark.skipif(condition, reason="..."): skip based on a condition.
  • @pytest.mark.xfail(reason="..."): mark test as expected to fail—useful during development.

Example skipping a test if Python version is below 3.8:

import sys
import pytest

@pytest.mark.skipif(sys.version_info < (3,8), reason="Requires Python 3.8+")
def test_new_feature():
    assert True

Capturing Output and Debugging

pytest can capture stdout/stderr output during test runs and display it only when a test fails. To see print statements live during testing, run with:

pytest -s

For debugging, you can insert breakpoint() in your test or code, then run pytest with --pdb to drop into the debugger on failures.

pytest Plugins and Extensions

One of the greatest strengths of pytest is its extensibility. Hundreds of plugins exist to add functionality such as coverage reporting, parallel execution, and more.

To check installed plugins, run:

pytest --fixtures

Popular plugins include:

  • pytest-cov: coverage measurement.
  • pytest-xdist: parallel test execution.
  • pytest-mock: easier mocking.

pytest vs unittest

While Python’s built-in unittest framework is powerful, pytest offers several advantages that make it preferred in modern Python development.

pytest vs unittest Comparison
Featurepytestunittest
Test discoveryAutomatic, based on naming conventionsManual test suite setup
Assertion stylePlain assert, with rich failure infoSpecific assert methods (assertEqual, etc.)
FixturesFlexible, modular fixtures with dependency injectionSetUp/tearDown methods per class
PluginsExtensive ecosystem of pluginsLimited plugin support
ParameterizationEasy parameterized tests with @pytest.mark.parametrizeRequires manual looping or third-party tools
Architecture of Testing with pytest
Architecture of Testing with pytest

Advanced: Parameterizing Tests

To avoid repetitive tests with different inputs, pytest allows parameterization:

📌 Deep Dive: Parameterized Test Example

import pytest
from math_ops import add

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

This runs test_add_multiple four times with different arguments, improving test coverage with minimal code.

Best Practices for Writing Tests with pytest

  • Name tests clearly: Use descriptive names to indicate what behavior you’re testing.
  • Test one thing per test: Keep tests focused for easier debugging.
  • Use fixtures for setup: Avoid duplicating setup code.
  • Run tests frequently: Integrate testing into your workflow or CI pipeline.
  • Keep tests deterministic: Avoid randomness or external dependencies without control.

Summary

Testing your Python code with pytest is straightforward and immensely valuable. From quick assertions to complex test suites with fixtures and plugins, pytest scales with your needs.

By writing tests, you ensure your code behaves correctly, reduce bugs, and gain confidence to iterate and improve your projects. Get comfortable with running pytest, structuring your tests, and exploring its features—you’re building a strong foundation for professional Python development.

💡 Remember:

Testing is not a one-time chore but an ongoing habit. The more you practice with tools like pytest, the more intuitive and rewarding it becomes.