testing-python
Write and evaluate effective Python tests using pytest, and reproduce/fix build failures against the project's own declared test runner (tox, nox, make, uv, pytest). Use when writing tests, reviewing test code, debugging test failures, fixing failed builds, or improving test coverage. Covers test design, fixtures, parameterization, mocking, async testing, and verifier-parity reproduction of CI/build failures.
- License 许可
- Apache-2.0
- 最近更新
- 2026-11-14
- Stars
- ★ 0
- Forks
- 1
01 前言 Front Matter
- name
- testing-python
- description
- Write and evaluate effective Python tests using pytest, and reproduce/fix build failures against the project's own declared test runner (tox, nox, make, uv, pytest). Use when writing tests, reviewing test code, debugging test failures, fixing failed builds, or improving test coverage. Covers test design, fixtures, parameterization, mocking, async testing, and verifier-parity reproduction of CI/build failures.
03 SKILL.md 正文
Writing Effective Python Tests
Core Principles
Every test should be atomic, self-contained, and test single functionality. A test that tests multiple things is harder to debug and maintain.
Reproducing Build / CI Failures (read this first when fixing a failed build)
When the task is "fix the build" or "make CI pass", the definition of success is the project's own declared runner exits 0, not pytest from an ad-hoc venv. Before editing anything, and again before declaring the task done, reproduce the failure using the same runner the verifier/CI uses.
Runner discovery checklist
Inspect the repo root for the authoritative runner, in this order:
tox.inior[tool.tox]inpyproject.toml→ runtox(ortox -e <env>). Theenvlistdefines every environment that must pass.noxfile.py→ runnox.Makefiletargets liketest,check,ci→ run those..github/workflows/*.yml→ read the exact command the CI job runs (run:steps). Mirror it locally.pyproject.toml[tool.pytest.ini_options]orpytest.ini→ only fall back to barepytestif nothing above exists.
If you only run bare pytest against a hand-picked interpreter, you can miss:
- Environments in the tox
envlistwhose Python interpreter is not installed (e.g.py37: could not find python interpreter matching any of the specs py37). These count as build failures. - Plugins loaded only inside the tox env (e.g.
pytest-asyncio,requests_mock) that change which tests are collected, skipped, or executed. - Version-pinned dependencies in
[testenv].depsorinstall_package_depsthat produce different behavior than your interactive venv.
Verifier-parity rules
- Do not classify any failing test as "pre-existing" or "out of scope" for a build-fix task. If the runner exits non-zero, the build is failing and the task is not done. Investigate the failure and fix it in code or configuration (skip markers, xfail with reason, dependency pin, environment guard, or a real code fix — pick the minimal correct option).
- Reproduce with the same interpreter matrix the runner uses. If
tox -llistspy37,py38,py39,py310,py311,py312and only 3.10/3.11/3.12 are installed on the runner, either install the missing interpreters or narrowenvlist/usetox --skip-missing-interpreters=true— whichever matches how the verifier is configured. Read the CI workflow to decide; do not guess. - Do not stop at "tests collect successfully". Collection succeeding is necessary but not sufficient. The runner must exit 0.
- Beware of silently-skipped tests. A test that is skipped in your local venv (e.g.
async defskipped becausepytest-asynciois not installed) will run and can fail under tox where the plugin is declared. Always match the plugin set from[testenv].deps.
Failure-triage order (build-fix tasks)
- Run the declared runner, capture full output.
- For each failing environment, categorize the failure:
- Environment/config: missing interpreter, missing dependency, wrong pin, wrong build backend. Fix in
tox.ini,pyproject.toml, workflow file, or by installing the interpreter. - Import-time failure: optional dependency imported unconditionally. Guard with
try/except ImportErrorat the import site, or move the import into the extra's own module and only expose it when installed. - Real test logic failure: fix the underlying code (or, if the test itself is wrong and the runner is authoritative, fix the test). Do not merely mask with
@pytest.mark.skipunless justified in a comment and matched by CI expectations.
- Environment/config: missing interpreter, missing dependency, wrong pin, wrong build backend. Fix in
- After each edit, rerun the declared runner (not bare pytest) and confirm exit 0 across the full env matrix.
Common concrete patterns
- Optional import at package
__init__: wrap intry/except ImportErrorso the base package imports without the extra, but keep the symbol exported when the extra is installed. toxcannot findpy37/py38/py39on modern runners: either (a) addskip_missing_interpreters = trueto[tox]if the CI job also sets it, (b) trimenvlistto the interpreters actually available in the CI image, or (c) install the missing interpreters viaactions/setup-pythonmatrix /uv python install. Match the CI workflow's real intent.- Timestamp / timing tests that assert
init_timestamp != end_timestamp: these fail deterministically when the timestamp helper truncates to millisecond precision and the event constructor +record()land in the same millisecond. Fix the helper (higher precision, monotonic sequence, or explicit sleep inrecord()), not the test — the test encodes the real invariant.
Test Structure
Atomic unit tests
Each test should verify a single behavior. The test name should tell you what's broken when it fails. Multiple assertions are fine when they all verify the same behavior.
# Good: Name tells you what's broken
def test_user_creation_sets_defaults():
user = User(name="Alice")
assert user.role == "member"
assert user.id is not None
assert user.created_at is not None
# Bad: If this fails, what behavior is broken?
def test_user():
user = User(name="Alice")
assert user.role == "member"
user.promote()
assert user.role == "admin"
assert user.can_delete_others()
Use parameterization for variations of the same concept
import pytest
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("World", "WORLD"),
("", ""),
("123", "123"),
])
def test_uppercase_conversion(input, expected):
assert input.upper() == expected
Use separate tests for different functionality
Don't parameterize unrelated behaviors. If the test logic differs, write separate tests.
Project-Specific Rules
These rules apply when the project you are working on declares them (e.g. via pyproject.toml, contribution docs, or fixture setup). They are not universal — read the target project's conventions first.
asyncio_mode = "auto"
If the project sets asyncio_mode = "auto" globally, write async tests without decorators:
# Correct (when asyncio_mode = "auto")
async def test_async_operation():
result = await some_async_function()
assert result == expected
# Wrong under asyncio_mode = "auto"
@pytest.mark.asyncio
async def test_async_operation():
...
If the project does NOT set auto mode, pytest-asyncio requires @pytest.mark.asyncio (or install a compatible plugin). An async test with neither is silently skipped by plain pytest — do not mistake that skip for success.
Imports at module level
Put ALL imports at the top of the file:
# Correct
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
async def test_something():
mcp = FastMCP("test")
...
Use in-memory transport for testing (fastmcp-style projects)
Pass FastMCP servers directly to clients:
from fastmcp import FastMCP
from fastmcp.client import Client
mcp = FastMCP("TestServer")
@mcp.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
async def test_greet_tool():
async with Client(mcp) as client:
result = await client.call_tool("greet", {"name": "World"})
assert result[0].text == "Hello, World!"
Only use HTTP transport when explicitly testing network features.
Inline snapshots for complex data
Use inline-snapshot for testing JSON schemas and complex structures:
from inline_snapshot import snapshot
def test_schema_generation():
schema = generate_schema(MyModel)
assert schema == snapshot() # Will auto-populate on first run
Commands:
pytest --inline-snapshot=create- populate empty snapshotspytest --inline-snapshot=fix- update after intentional changes
Fixtures
Prefer function-scoped fixtures
@pytest.fixture
def client():
return Client()
async def test_with_client(client):
result = await client.ping()
assert result is not None
Use tmp_path for file operations
def test_file_writing(tmp_path):
file = tmp_path / "test.txt"
file.write_text("content")
assert file.read_text() == "content"
Mocking
Mock at the boundary
from unittest.mock import patch, AsyncMock
async def test_external_api_call():
with patch("mymodule.external_client.fetch", new_callable=AsyncMock) as mock:
mock.return_value = {"data": "test"}
result = await my_function()
assert result == {"data": "test"}
Don't mock what you own
Test your code with real implementations when possible. Mock external services, not internal classes.
Test Naming
Use descriptive names that explain the scenario:
# Good
def test_login_fails_with_invalid_password():
def test_user_can_update_own_profile():
def test_admin_can_delete_any_user():
# Bad
def test_login():
def test_update():
def test_delete():
Error Testing
import pytest
def test_raises_on_invalid_input():
with pytest.raises(ValueError, match="must be positive"):
calculate(-1)
async def test_async_raises():
with pytest.raises(ConnectionError):
await connect_to_invalid_host()
Running Tests
Prefer the project's declared runner. Common invocations:
# Project uses tox — this is the source of truth for CI
tox # run full envlist
tox -e py311 # single env
tox -l # list envs the project declares
tox --skip-missing-interpreters=true # if CI also skips missing
# Project uses nox
nox
nox -s tests
# Project uses uv
uv run pytest
uv sync --all-extras --dev && uv run pytest
# Bare pytest (only when the project has no higher-level runner)
pytest -n auto # Run all tests in parallel
pytest -n auto -x # Stop on first failure
pytest path/to/test.py # Run specific file
pytest -k "test_name" # Run tests matching pattern
pytest -m "not integration" # Exclude integration tests
Always read .github/workflows/*.yml to confirm the exact command CI runs, and mirror it locally before declaring a build-fix task complete.
Checklist
Before submitting tests:
- [ ] Each test tests one thing
- [ ] Async decorator matches project's
asyncio_modesetting - [ ] Imports at module level
- [ ] Descriptive test names
- [ ] Parameterization for variations of same behavior
- [ ] Separate tests for different behaviors
Before declaring a build-fix task done:
- [ ] Identified the project's real test runner (tox / nox / make / uv / pytest)
- [ ] Ran that runner and captured the failing envs
- [ ] Every env in the runner's matrix exits 0 (not just "pytest passes on my venv")
- [ ] No failure was dismissed as "pre-existing" or "out of scope" without a code-level fix, a justified skip/xfail, or a config change that matches CI
- [ ] Missing interpreters in the tox matrix are either installed or excluded in a way consistent with the CI workflow
04 同仓库更多 Skills
video-frame-extraction
Extract frames from video files and save them as images using OpenCV
openai-vision
Analyze images and multi-frame sequences using OpenAI GPT vision models
image-ocr
Extract text content from images using Tesseract OCR via Python, including structured field extraction from receipts and forms
video-frame-extraction
Extract frames from video files and save them as images using OpenCV
openai-vision
Analyze images and multi-frame sequences using OpenAI GPT vision models
image-ocr
Extract text content from images using Tesseract OCR via Python, including structured field extraction from receipts and forms