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
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__.pyAdding 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 / bCreating 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
coverage.py measures which lines of code your tests execute. Add it as a development dependency:
$ uv add --dev coverage
Run your tests through coverage:
$ uv run coverage run -m pytest
Then view the report:
$ uv run coverage report
To see which specific lines were missed:
$ uv run coverage report -m
Tip
You may see pytest-cov recommended elsewhere. It wraps coverage.py with a --cov flag for pytest. Using coverage directly is one fewer dependency and teaches you the tool that’s doing the actual work.
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"Now re-run pytest on the command line. It will automatically run with this option set, stopping after the first failure.
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.0When 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
- How to test against multiple Python versions using uv shows how to run tests across Python 3.10, 3.11, 3.12, and beyond
- How to run tests using uv covers additional ways to invoke pytest in uv projects
- How to run tests in parallel with pytest-xdist speeds up large test suites by distributing tests across CPUs
- How to fix common pytest errors with uv helps troubleshoot import errors and other setup issues
- Setting up GitHub Actions with uv shows how to run tests automatically on every push
- How to set up pre-commit hooks for a Python project adds automated code checks before each commit
- Set up Ruff for formatting and checking your code pairs well with a test suite for maintaining code quality
- What are Optional Dependencies and Dependency Groups? explains how
--devdependencies work under the hood
This handbook is free, independent, and ad-free. If it saved you time, consider sponsoring it on GitHub.