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
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=1stops after the first failure--cov=testing_demogenerates 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.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
- 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
Also Mentioned In
- uv: A Complete Guide to Python's Fastest Package Manager
- How to configure Cursor for a uv project
- How to configure VS Code for a uv project
- How to Fix Common pytest Errors with uv
- How to Run Tests Using uv
- How to Test Against Multiple Python Versions Using uv
- Modern Python Project Setup Guide for AI Assistants
- pytest: Python Testing Framework
- Setting up GitHub Actions with uv
- uv: Python Package and Project Manager
Get Python tooling updates
Subscribe to the newsletter