# Mastering Python Testing with pytest: Best Practices Explained
URL: https://madhudadi.in/blog/posts/python-testing-best-practices-with-pytest-a-complete-guide
Published: 2026-06-26
Tags: python, Best Practices, Pytest
Read time: 42 min
Difficulty: intermediate
> Learn Python testing best practices with pytest: test folder layout, naming, assertions, fixtures, parametrize, temporary files, monkeypatching, mocking APIs, testing errors, coverage, and CI.# Python Testing Best Practices with pytest: Structure, Fixtures, Parametrize, Mocking, and CI

## Quick Answer

Use `pytest` with a clear `tests/` folder, small focused tests, reusable fixtures, and one command that runs the full suite.

Recommended layout:

```text
project/
  src/
    my_app/
      calculator.py
      parser.py
  tests/
    test_calculator.py
    test_parser.py
  pyproject.toml
```

Run tests:

```bash
pytest
```

Last verified: June 26, 2026.

## Official Links

- pytest docs: https://docs.pytest.org/
- pytest good practices: https://docs.pytest.org/en/stable/explanation/goodpractices.html
- unittest.mock: https://docs.python.org/3/library/unittest.mock.html
- tempfile: https://docs.python.org/3/library/tempfile.html

## What You Will Learn

By the end, you should be able to:

- create a clean `tests/` folder
- name test files and test functions correctly
- write simple pytest assertions
- test success and failure cases
- use fixtures
- use `parametrize`
- test temporary files
- mock environment variables
- mock slow or external services
- add tests to CI

## 1. Why Tests Matter

Tests protect behavior.

They help you answer:

- Does the code still work after a change?
- Did I break an edge case?
- Can another developer refactor safely?
- Can CI catch problems before deployment?

Tests are not only for large companies. They are useful for portfolio projects, scripts, CLIs, APIs, and data pipelines.

## 2. Install pytest

```bash
python -m pip install pytest
```

If you use `pyproject.toml`, add pytest as a development dependency:

```toml
[project.optional-dependencies]
dev = [
  "pytest>=8.0",
]
```

Install:

```bash
python -m pip install -e ".[dev]"
```

## 3. Use A Dedicated Tests Folder

Good:

```text
tests/
  test_prices.py
  test_parsing.py
  test_config.py
```

Avoid mixing tests into random folders.

Pytest discovers:

- files named `test_*.py`
- functions named `test_*`

Example:

```python
def test_total_price_includes_tax():
    assert 118 == 100 + 18
```

## 4. Keep Tests Small

Small tests are easier to understand.

Weak test:

```python
def test_everything():
    # creates data
    # parses data
    # saves file
    # calls API
    # checks output
    ...
```

Better:

```python
def test_parse_price_removes_currency_symbol():
    assert parse_price("Rs.1,999") == 1999
```

One test should usually verify one behavior.

## 5. Test Pure Functions First

Pure functions are easiest to test.

Application code:

```python
def discount_pct(actual_price: float, discounted_price: float) -> float:
    if actual_price <= 0:
        raise ValueError("actual_price must be positive")
    return round((actual_price - discounted_price) / actual_price * 100, 1)
```

Test:

```python
def test_discount_pct():
    assert discount_pct(1000, 750) == 25.0
```

Start testing with calculation, parsing, validation, and transformation logic.

## 6. Test Error Cases

Use `pytest.raises`.

```python
import pytest

def test_discount_pct_rejects_zero_actual_price():
    with pytest.raises(ValueError, match="actual_price"):
        discount_pct(0, 100)
```

Error tests prevent silent bad data.

## 7. Use Parametrize For Similar Cases

Instead of repeating similar tests:

```python
import pytest

@pytest.mark.parametrize(
    "raw, expected",
    [
        ("Rs.1,999", 1999),
        ("1999", 1999),
        ("1,999", 1999),
    ],
)
def test_parse_price(raw, expected):
    assert parse_price(raw) == expected
```

Parametrized tests are concise and readable.

## 8. Use Fixtures For Reusable Setup

Fixtures prepare reusable test data.

```python
import pytest

@pytest.fixture
def sample_order():
    return {
        "price": 1000,
        "discounted_price": 750,
        "customer": "Aarav",
    }

def test_order_discount(sample_order):
    assert discount_pct(sample_order["price"], sample_order["discounted_price"]) == 25.0
```

Use fixtures for:

- sample dictionaries
- temporary databases
- test clients
- fake API responses
- shared setup

Do not hide important test logic inside overly complex fixtures.

## 9. Test Files With `tmp_path`

Use pytest's `tmp_path` for temporary files.

```python
def save_report(path, text):
    path.write_text(text, encoding="utf-8")

def test_save_report(tmp_path):
    report_path = tmp_path / "report.txt"

    save_report(report_path, "hello")

    assert report_path.read_text(encoding="utf-8") == "hello"
```

Never write tests that depend on your desktop path or real downloads folder.

## 10. Test Environment Variables With monkeypatch

Use `monkeypatch`.

```python
import os

def load_api_url():
    return os.environ.get("API_URL", "http://localhost:8000")

def test_load_api_url_from_env(monkeypatch):
    monkeypatch.setenv("API_URL", "https://example.com")

    assert load_api_url() == "https://example.com"
```

This keeps tests isolated.

## 11. Mock External Calls

Do not call real APIs in unit tests.

Application code:

```python
def fetch_user(http_client, user_id: int) -> dict:
    response = http_client.get(f"/users/{user_id}")
    return response.json()
```

Test:

```python
from unittest.mock import Mock

def test_fetch_user():
    response = Mock()
    response.json.return_value = {"id": 1, "name": "Aarav"}

    http_client = Mock()
    http_client.get.return_value = response

    assert fetch_user(http_client, 1)["name"] == "Aarav"
    http_client.get.assert_called_once_with("/users/1")
```

Mock network, payment, email, and slow services.

## 12. Test Classes By Behavior

Do not test private implementation details unless necessary.

```python
class Cart:
    def __init__(self):
        self.items = []

    def add(self, price: float):
        self.items.append(price)

    def total(self) -> float:
        return sum(self.items)
```

Test:

```python
def test_cart_total():
    cart = Cart()
    cart.add(100)
    cart.add(50)

    assert cart.total() == 150
```

Focus on public behavior.

## 13. Separate Unit And Integration Tests

Unit tests:

- fast
- isolated
- no real network
- no real external services

Integration tests:

- test multiple parts together
- may use a test database
- may be slower
- should be clearly marked

Example marker:

```python
import pytest

@pytest.mark.integration
def test_database_insert_and_read():
    ...
```

Configure markers:

```toml
[tool.pytest.ini_options]
markers = [
  "integration: tests that touch external systems or multiple layers",
]
```

## 14. Avoid Flaky Tests

Flaky tests pass sometimes and fail sometimes.

Common causes:

- real network calls
- current time
- random values
- shared global state
- test order dependency
- sleeping instead of waiting for a condition

Make tests deterministic.

If code uses randomness, inject a seed or pass values directly.

## 15. Use Coverage Carefully

Install:

```bash
python -m pip install pytest-cov
```

Run:

```bash
pytest --cov=src --cov-report=term-missing
```

Coverage is useful, but high coverage does not guarantee good tests.

Focus on important behavior and edge cases.

## 16. Add pytest Configuration

In `pyproject.toml`:

```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
```

This makes test discovery predictable.

## 17. Add Tests To CI

GitHub Actions example:

```yaml
name: Python tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: python -m pip install -e ".[dev]"
      - run: pytest
```

CI makes tests run the same way for everyone.

## 18. Testing Checklist

Before sharing a project, confirm:

- tests live in `tests/`
- test files start with `test_`
- tests run with `pytest`
- important success cases are covered
- important failure cases are covered
- external APIs are mocked in unit tests
- file tests use `tmp_path`
- environment tests use `monkeypatch`
- CI runs tests

## 19. FAQ

### Is pytest better than unittest?

Both work. Pytest is popular because it has simple assertions, fixtures, parametrization, and strong plugin support.

### How many tests should a beginner project have?

Start with tests for the most important functions. Even five good tests are useful.

### Should I test private functions?

Usually test public behavior. If private logic is complex, consider extracting it into a public helper with a clear purpose.

### Should tests call real APIs?

Not in unit tests. Use mocks. Put live API checks behind explicit integration tests.

### What should I test first?

Test parsing, validation, calculations, transformations, and error handling first.
