Part 5: Testing with pytest and Modern Packaging

Introduction

ansible-inspec uses pytest with async support, fixture-based test isolation, and httpx.AsyncClient for API integration tests. All of this is configured through pyproject.toml. This part covers the test patterns I actually use and how the project is packaged for PyPI.


pytest Basics

Installation

pip install pytest pytest-asyncio httpx

Minimal test

# tests/unit/test_converter.py

def add(a: int, b: int) -> int:
    return a + b

def test_add() -> None:
    assert add(2, 3) == 5

def test_add_negative() -> None:
    assert add(-1, 1) == 0

Run:

Assertions — just use assert


Fixtures

Fixtures are pytest's dependency injection system. They set up (and tear down) resources for tests.

Basic fixture

Fixture scope

Fixture with teardown

conftest.py — shared fixtures across files

Pytest automatically loads conftest.py from the test directory. Put shared fixtures there:


Async Tests with pytest-asyncio

All async tests need pytest-asyncio. Configure it in pyproject.toml:

With asyncio_mode = "auto", any async def test_* function is automatically treated as an async test:

Async fixtures


Testing FastAPI with httpx.AsyncClient

The cleanest way to integration-test FastAPI without a real server:


Mocking with unittest.mock

For tests that shouldn't hit real external systems (Git, SSH, file system):

pytest-mock — cleaner mock API


Parametrize — test multiple inputs


Test Organisation

Structure mirrors the source tree:

Run by layer:


Code Quality Tools

ruff — linter + formatter (replaces flake8 + black)

pyproject.toml config:

mypy — static type checking

pyproject.toml config:

Makefile bringing it together


GitHub Actions CI

ansible-inspec runs tests on every push:


Publishing to PyPI

ansible-inspec publishes automatically on a git tag via GitHub Actions:

Manual build and publish:


Full pyproject.toml Reference


Summary

Concept
Key point

pytest fixtures

Reusable, scoped setup/teardown via @pytest.fixture

conftest.py

Shared fixtures visible to all test files in the directory

pytest-asyncio

asyncio_mode = "auto" — no extra markers needed

httpx.AsyncClient

In-process API tests with ASGITransport — no server needed

unittest.mock

Patch external systems; AsyncMock for async functions

@pytest.mark.parametrize

Test multiple inputs in one test function

ruff

Linter + formatter replacing flake8 + black

mypy --strict

Catch type errors before runtime

python -m build

Build sdist + wheel from pyproject.toml

Trusted publishing

PyPI publish via OIDC — no API token to manage


Series Complete

This series covered Python 3.12 from first setup through production-ready packaging:

Part
Topic

1

Environment, pyproject.toml, Python 3.12 language features

2

Data structures, type hints, Pydantic v2

3

OOP, @dataclass, Protocol, ABC

4

asyncio, async/await, FastAPI

5

pytest, async tests, CI, PyPI packaging

All code patterns in this series are drawn from ansible-inspecarrow-up-right — a real open-source tool worth exploring if you want to see these patterns at scale.

Last updated