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

258 lines
6.4 KiB
Markdown

# Testing and Code Quality
## Table of Contents
1. [conftest.py](#1-conftestpy)
2. [Unit tests](#2-unit-tests)
3. [Backend unit tests](#3-backend-unit-tests)
4. [Running tests](#4-running-tests)
5. [Code quality tools](#5-code-quality-tools)
6. [Pre-commit hooks](#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`.
```python
# 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).
```python
# 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.
```python
# 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
```bash
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)
```bash
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)
```bash
pip install black
black . # Format all files
black . --check # CI mode — reports issues without modifying files
```
### isort (import sorting)
```bash
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)
```bash
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
```bash
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`.
```yaml
# .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]
```
```bash
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.