ThisCalculator

Everyday

Unit Testing Python with pytest: A Practical Guide

Updated · Standard formula, shown below

Unit Testing Python with pytest: A Practical Guide

To unit test Python code with pytest, write small tests in files named test_*.py or *_test.py, then run pytest from your project directory. pytest discovers those tests, calls the functions they exercise, and reports whether their assertions pass or fail.

What pytest does

A unit test checks a small piece of software—often one function or method—against an expected result. The test supplies an input, runs the code, and checks the output or behavior. Tests help catch regressions when code changes, clarify intended behavior, and make failures easier to reproduce.

pytest is a widely used Python testing framework. It can run ordinary Python test functions and supports features such as fixtures, parameterized tests, and plugins. A key convenience is that Python’s built-in assert statement works directly in tests; pytest provides detailed explanations when an assertion fails.

Unit tests are not a guarantee that a whole application works. A test may pass while an interaction with a database, network service, or user interface is broken. Those broader interactions are usually covered with integration or end-to-end tests.

Install pytest and write a first test

Install pytest in the same Python environment where you intend to run it:

python -m pip install pytest

Using python -m pip helps ensure that pip belongs to the selected Python interpreter. If your project uses a virtual environment, activate it first. Then create a small module and its test. For example, put this in math_utils.py:

def add(a, b):
    return a + b

Create test_math_utils.py alongside it:

from math_utils import add


def test_adds_two_numbers():
    assert add(2, 3) == 5

Run the suite with:

python -m pytest

pytest searches the current directory and its subdirectories for files matching its test-file naming patterns, then looks for test functions whose names start with test_. It displays a summary, including the number of passed and failed tests. The command pytest is also common; python -m pytest makes the interpreter selection explicit.

Organize tests and choose useful assertions

For a small project, keeping test files near the code can be convenient. Larger projects often use a top-level tests/ directory. Either approach can work; consistency and clear imports matter more than a single mandatory layout.

A useful test focuses on one behavior and makes its expected outcome obvious. For example, if a function should reject a negative quantity, test that contract explicitly rather than burying it in an unrelated test. Tests can check returned values, changed state, or raised exceptions.

import pytest
from math_utils import add


def test_add_handles_negative_number():
    assert add(-2, 5) == 3


def test_invalid_input_raises_value_error():
    with pytest.raises(ValueError):
        parse_quantity("not a number")

The second example assumes parse_quantity is defined elsewhere and raises ValueError for that input. The pytest.raises context manager passes only if the expected exception occurs; if no exception occurs, the test fails. Avoid catching every exception indiscriminately, since that can hide unrelated defects.

Use descriptive names such as test_empty_input_returns_empty_list. A test name should communicate the scenario and expected behavior, especially when it fails in continuous integration or a large test run.

Use parameterization for related cases

When the same rule should hold for several inputs, pytest’s @pytest.mark.parametrize decorator avoids repetitive test functions. Each row supplies arguments for one run:

import pytest
from math_utils import add


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

Each parameter set is reported as a separate test case. This makes edge cases visible while keeping the shared behavior in one place. Do not combine unrelated scenarios just to reduce the number of functions: separate tests can be easier to understand and diagnose.

Fixtures and setup

Fixtures provide reusable setup or resources to tests. Define one with @pytest.fixture, then request it by naming it as a test function parameter. pytest supplies the fixture value when it runs the test.

import pytest


@pytest.fixture
def sample_items():
    return ["pear", "plum"]


def test_item_count(sample_items):
    assert len(sample_items) == 2

Fixtures are useful for shared objects, temporary files, and controlled test data. A fixture can also use yield to run cleanup after a test, which matters for resources that need closing or removal. Keep setup focused: complicated fixtures can obscure what a test actually verifies.

pytest includes fixtures such as tmp_path for a temporary directory. For example, a test can write a file beneath that path without relying on a permanent file in the project. This keeps tests more isolated and repeatable.

Run selected tests and diagnose failures

You do not need to run every test after every small edit. Common commands include:

  • python -m pytest tests/test_math_utils.py to run one file.
  • python -m pytest -k add to select tests whose names match an expression containing add.
  • python -m pytest -q for a more concise summary.
  • python -m pytest -v to list individual test names and results.

When a test fails, read the assertion details and traceback from the first relevant frame. A failure means either the program does not meet the behavior the test describes, or the test’s expectation is wrong. Confirm the intended contract before changing code simply to make a test green.

Tests should be repeatable and independent where possible. If one test relies on another having run first, results can depend on execution order. Avoid unnecessary network calls and shared mutable state; use controlled inputs and temporary resources so a test can run offline and repeatedly.

Frequently Asked Questions

How do I run pytest for one test?
Run python -m pytest path/to/test_file.py::test_function_name, replacing the path and function name with yours.

Why is pytest not finding my tests?
Check that the file is named test_*.py or *_test.py, the function starts with test_, and you are running pytest from the intended project directory and Python environment.

Do pytest tests need a special class?
No. A plain function whose name begins with test_ is enough. Test classes are optional and have their own discovery naming conventions.

What is the difference between pytest and unittest?
unittest is included in Python’s standard library and commonly uses test classes and assertion methods. pytest is an installable third-party framework that also runs many unittest-style tests and offers features such as fixtures and parameterization. The better choice depends on project needs and conventions.

Should unit tests use a real database or API?
Usually, a focused unit test avoids live external services so it stays fast and repeatable. Test real integrations separately, or replace external dependencies with controlled substitutes when testing a unit in isolation.

Does a passing test prove the code is correct?
No. It shows that the tested cases met their assertions. Tests cannot cover every input or interaction, so choose representative cases and combine unit tests with other appropriate checks.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.