Skip to content

Setting up testing with pytest and uv

Every Python project needs tests, but setting up a test suite from scratch involves decisions about project layout, dependency management, and configuration. This tutorial walks through the full setup using pytest and uv: creating a project, writing tests, using fixtures, measuring coverage, and configuring defaults.

Prerequisites

Install uv on your system.

Creating a Project with Tests

Start by creating a sample project with a test directory structure:

$ uv init testing-demo --package
$ cd testing-demo

This creates a Python package project with the following structure:

testing-demo/
├── pyproject.toml
├── README.md
└── src
    └── testing_demo
        └── __init__.py

Adding pytest as a Development Dependency

Add pytest to your project’s development dependencies:

$ uv add --dev pytest

This command:

  • Updates your pyproject.toml with pytest as a development dependency
  • Creates the project’s lockfile
  • Installs pytest in your project’s virtual environment

Creating a Simple Module to Test

Create a calculator module at src/testing_demo/calculator.py:

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


def subtract(a, b):
    return a - b


def multiply(a, b):
    return a * b


def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

Creating Test Files

Create a tests directory at the root of your project:

$ mkdir tests

Now, create a test file for the calculator module in tests/test_calculator.py:

import pytest
from testing_demo.calculator import add, subtract, multiply, divide


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


def test_subtract():
    assert subtract(3, 2) == 1
    assert subtract(2, 3) == -1
    assert subtract(0, 0) == 0


def test_multiply():
    assert multiply(2, 3) == 6
    assert multiply(-2, 3) == -6
    assert multiply(-2, -3) == 6


def test_divide():
    assert divide(6, 3) == 2
    assert divide(6, -3) == -2
    assert divide(-6, -3) == 2


def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(5, 0)

Running Tests

Run tests using uv:

$ uv run pytest

To see more detailed output, use the verbose flag:

$ uv run pytest -v

Adding Test Coverage

Add coverage reporting to see how much of the code is tested:

$ uv add --dev pytest-cov

Now run tests with coverage:

$ uv run pytest --cov=testing_demo

For a more detailed report:

$ uv run pytest --cov=testing_demo --cov-report=term-missing

Configuring Pytest

Customize the default options when running pytest by adding the following to your pyproject.toml file:

[tool.pytest.ini_options]
addopts = "--maxfail=1 --cov=testing_demo"

Now re-run pytest on the command line. It will automatically run with these options set:

  • --maxfail=1 stops after the first failure
  • --cov=testing_demo generates the coverage report

Using Fixtures

Fixtures let you define reusable setup code that pytest injects into test functions automatically. They replace the setup/teardown pattern from unittest with something more composable.

Add this test file at tests/test_calculator_with_fixtures.py:

import pytest
from testing_demo.calculator import add, subtract, multiply, divide


@pytest.fixture
def sample_numbers():
    """Provide a pair of numbers for testing."""
    return (10, 5)


def test_add_with_fixture(sample_numbers):
    a, b = sample_numbers
    assert add(a, b) == 15


def test_subtract_with_fixture(sample_numbers):
    a, b = sample_numbers
    assert subtract(a, b) == 5


def test_multiply_with_fixture(sample_numbers):
    a, b = sample_numbers
    assert multiply(a, b) == 50


def test_divide_with_fixture(sample_numbers):
    a, b = sample_numbers
    assert divide(a, b) == 2.0

When pytest sees sample_numbers as a parameter name, it looks for a fixture with that name and passes its return value into the test. This keeps test data in one place and makes tests shorter.

Fixtures can also be shared across multiple test files by placing them in a tests/conftest.py file. Any fixture defined in conftest.py is available to all tests in the same directory and its subdirectories.

Running Specific Tests

As a test suite grows, running every test on each change slows you down. pytest provides several ways to run a subset:

Run a single test file:

$ uv run pytest tests/test_calculator.py

Run a single test function:

$ uv run pytest tests/test_calculator.py::test_add

Run tests matching a keyword expression:

$ uv run pytest -k "divide"

This runs only tests whose names contain “divide”, which in this project matches both test_divide and test_divide_by_zero.

Final Project Structure

After completing this tutorial, the project looks like this:

    • pyproject.toml
    • README.md
        • init.py
        • calculator.py
      • test_calculator.py
      • test_calculator_with_fixtures.py

Next Steps

Get Python tooling updates

Subscribe to the newsletter
Last updated on

Please submit corrections and feedback...