Files
awesome-copilot/skills/python-pypi-package-builder/references/testing-quality.md
Patel Dhruv 49fd3f3faf Add new skill: Python PyPI Package Builder (#1302)
* Add python-pypi-package-builder skill for Python packaging

- Created `SKILL.md` defining decision-driven workflow for building, testing, versioning, and publishing Python packages.
- Added reference modules covering PyPA packaging, architecture patterns, CI/CD, testing, versioning strategy, and release governance.
- Implemented scaffold script to generate complete project structure with pyproject.toml, CI workflows, tests, and configuration.
- Included support for multiple build backends (setuptools_scm, hatchling, flit, poetry) with clear decision rules.
- Added secure release practices including tag-based versioning, branch protection, and OIDC Trusted Publishing.

* fix: correct spelling issues detected by codespell
2026-04-09 10:36:17 +10:00

6.4 KiB

Testing and Code Quality

Table of Contents

  1. conftest.py
  2. Unit tests
  3. Backend unit tests
  4. Running tests
  5. Code quality tools
  6. Pre-commit hooks

1. conftest.py

Use conftest.py to define shared fixtures. Keep fixtures focused — one fixture per concern. For async tests, use pytest-asyncio with asyncio_mode = "auto" in pyproject.toml.

# tests/conftest.py
import pytest
from your_package.core import YourClient
from your_package.backends.memory import MemoryBackend


@pytest.fixture
def memory_backend() -> MemoryBackend:
    return MemoryBackend()


@pytest.fixture
def client(memory_backend: MemoryBackend) -> YourClient:
    return YourClient(
        api_key="test-key",
        backend=memory_backend,
    )

2. Unit Tests

Test both the happy path and the edge cases (e.g. invalid inputs, error conditions).

# tests/test_core.py
import pytest
from your_package import YourClient
from your_package.exceptions import YourPackageError


def test_client_creates_with_valid_key():
    client = YourClient(api_key="sk-test")
    assert client is not None


def test_client_raises_on_empty_key():
    with pytest.raises(ValueError, match="api_key"):
        YourClient(api_key="")


def test_client_raises_on_invalid_timeout():
    with pytest.raises(ValueError, match="timeout"):
        YourClient(api_key="sk-test", timeout=-1)


@pytest.mark.asyncio
async def test_process_returns_expected_result(client: YourClient):
    result = await client.process({"input": "value"})
    assert "output" in result


@pytest.mark.asyncio
async def test_process_raises_on_invalid_input(client: YourClient):
    with pytest.raises(YourPackageError):
        await client.process({})  # empty input should fail

3. Backend Unit Tests

Test each backend independently, in isolation from the rest of the library. This makes failures easier to diagnose and ensures your abstract interface is actually implemented correctly.

# tests/test_backends.py
import pytest
from your_package.backends.memory import MemoryBackend


@pytest.mark.asyncio
async def test_set_and_get():
    backend = MemoryBackend()
    await backend.set("key1", "value1")
    result = await backend.get("key1")
    assert result == "value1"


@pytest.mark.asyncio
async def test_get_missing_key_returns_none():
    backend = MemoryBackend()
    result = await backend.get("nonexistent")
    assert result is None


@pytest.mark.asyncio
async def test_delete_removes_key():
    backend = MemoryBackend()
    await backend.set("key1", "value1")
    await backend.delete("key1")
    result = await backend.get("key1")
    assert result is None


@pytest.mark.asyncio
async def test_ttl_expires_entry():
    import asyncio
    backend = MemoryBackend()
    await backend.set("key1", "value1", ttl=1)
    await asyncio.sleep(1.1)
    result = await backend.get("key1")
    assert result is None


@pytest.mark.asyncio
async def test_different_keys_are_independent():
    backend = MemoryBackend()
    await backend.set("key1", "a")
    await backend.set("key2", "b")
    assert await backend.get("key1") == "a"
    assert await backend.get("key2") == "b"
    await backend.delete("key1")
    assert await backend.get("key2") == "b"

4. Running Tests

pip install -e ".[dev]"
pytest                           # All tests
pytest --cov --cov-report=html   # With HTML coverage report (opens in browser)
pytest -k "test_middleware"      # Filter by name
pytest -x                        # Stop on first failure
pytest -v                        # Verbose output

Coverage config in pyproject.toml enforces a minimum threshold (fail_under = 80). CI will fail if you drop below it, which catches coverage regressions automatically.


5. Code Quality Tools

Ruff (linting — replaces flake8, pylint, many others)

pip install ruff
ruff check .           # Check for issues
ruff check . --fix     # Auto-fix safe issues

Ruff is extremely fast and replaces most of the Python linting ecosystem. Configure it in pyproject.toml — see references/pyproject-toml.md for the full config.

Black (formatting)

pip install black
black .                # Format all files
black . --check        # CI mode — reports issues without modifying files

isort (import sorting)

pip install isort
isort .                # Sort imports
isort . --check-only   # CI mode

Always set profile = "black" in [tool.isort] — otherwise black and isort conflict.

mypy (static type checking)

pip install mypy
mypy your_package/   # Type-check your package source only

Common fixes:

  • ignore_missing_imports = true — ignore untyped third-party deps
  • from __future__ import annotations — enables PEP 563 deferred evaluation (Python 3.9 compat)
  • pip install types-redis — type stubs for the redis library

Run all at once

ruff check . && black . --check && isort . --check-only && mypy your_package/

6. Pre-commit Hooks

Pre-commit runs all quality tools automatically before each commit, so issues never reach CI. Install once per clone with pre-commit install.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black

  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        additional_dependencies: [types-redis]  # Add stubs for typed dependencies

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-merge-conflict
      - id: debug-statements
      - id: no-commit-to-branch
        args: [--branch, master, --branch, main]
pip install pre-commit
pre-commit install           # Install once per clone
pre-commit run --all-files   # Run all hooks manually (useful before the first install)

The no-commit-to-branch hook prevents accidentally committing directly to main/master, which would bypass CI checks. Always work on a feature branch.