Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Python testing with pytest using uv package manager. Use when: (1) Running Python tests, (2) Writing test files or test functions, (3) Setting up fixtures, (4) Parametrizing tests, (5) Generating coverage reports, (6) Testing FastAPI applications, (7) Debugging test failures, (8) Configuring pytest options. Triggers: "run tests", "write tests", "test coverage", "pytest", "unit test", "integration test", "test FastAPI".
.claude/skills/aiskillstore-pytest-mastery/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-09 | ✗→✓ | ▲ Improved | 328% | 0% |
| case-05 | ✓→✗ | ▼ Worse | 179% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 194% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 241% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 294% | 0% |
bash# Run all tests uv run pytest # Run with verbose output uv run pytest -v # Run specific file uv run pytest tests/test_example.py # Run specific test function uv run pytest tests/test_example.py::test_function_name # Run tests matching pattern uv run pytest -k "pattern" # Run with coverage uv run pytest --cov=src --cov-report=html
bash# Add pytest as dev dependency uv add --dev pytest # Add coverage support uv add --dev pytest-cov # Add async support (for FastAPI) uv add --dev pytest-asyncio httpx
pytest automatically discovers tests following these conventions:
test_*.py or *_test.pytest_*Test* (no __init__ method)test_* inside Test* classesStandard project structure:
project/
├── src/
│ └── myapp/
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures
│ ├── test_unit.py
│ └── integration/
│ └── test_api.py
└── pyproject.tomlFixtures provide reusable test setup/teardown:
pythonimport pytest @pytest.fixture def sample_user(): return {"id": 1, "name": "Test User"} @pytest.fixture def db_connection(): conn = create_connection() yield conn # Test runs here conn.close() # Teardown def test_user_name(sample_user): assert sample_user["name"] == "Test User"
python@pytest.fixture(scope="function") # Default: new instance per test @pytest.fixture(scope="class") # Once per test class @pytest.fixture(scope="module") # Once per module @pytest.fixture(scope="session") # Once per test session
Place in tests/conftest.py for automatic availability:
python# tests/conftest.py import pytest @pytest.fixture def api_client(): return TestClient(app)
Run same test with multiple inputs:
pythonimport pytest @pytest.mark.parametrize("input,expected", [ (1, 2), (2, 4), (3, 6), ]) def test_double(input, expected): assert input * 2 == expected @pytest.mark.parametrize("value", [None, "", [], {}]) def test_falsy_values(value): assert not value
| Option | Description | |--------|-------------| | -v | Verbose output | | -vv | More verbose | | -q | Quiet mode | | -x | Stop on first failure | | --lf | Run last failed tests only | | --ff | Run failures first | | -k "expr" | Filter by name expression | | -m "mark" | Run marked tests only | | --tb=short | Shorter traceback | | --tb=no | No traceback | | -s | Show print statements | | --durations=10 | Show 10 slowest tests | | -n auto | Parallel execution (pytest-xdist) |
bash# Terminal report uv run pytest --cov=src # HTML report (creates htmlcov/) uv run pytest --cov=src --cov-report=html # With minimum threshold (fails if below) uv run pytest --cov=src --cov-fail-under=80 # Multiple report formats uv run pytest --cov=src --cov-report=term --cov-report=xml
pythonimport pytest @pytest.mark.slow def test_slow_operation(): ... @pytest.mark.skip(reason="Not implemented") def test_future_feature(): ... @pytest.mark.skipif(condition, reason="...") def test_conditional(): ... @pytest.mark.xfail(reason="Known bug") def test_known_failure(): ...
Run by marker:
bashuv run pytest -m "not slow" uv run pytest -m "integration"
toml[tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_functions = ["test_*"] addopts = "-v --tb=short" markers = [ "slow: marks tests as slow", "integration: integration tests", ] [tool.coverage.run] source = ["src"] omit = ["tests/*", "*/__init__.py"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", ]
See references/fastapi-testing.md for comprehensive FastAPI testing patterns including:
bash# Run with full traceback uv run pytest --tb=long # Drop into debugger on failure uv run pytest --pdb # Show local variables in traceback uv run pytest -l # Run only previously failed uv run pytest --lf
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 4,355 | 3,881 | -11% | 1 | 1 | 0% | 684 | 2,012 | +194% | 0 | 0 | — |
case-02 | pass→pass | 3,312 | 2,149 | -35% | 1 | 1 | 0% | 484 | 1,650 | +241% | 0 | 0 | — |
case-03 | pass→pass | 3,177 | 2,455 | -23% | 1 | 1 | 0% | 438 | 1,727 | +294% | 0 | 0 | — |
case-04 | pass→pass | 3,838 | 2,861 | -25% | 1 | 1 | 0% | 585 | 1,791 | +206% | 0 | 0 | — |
case-05 | pass→fail | 4,490 | 3,355 | -25% | 1 | 1 | 0% | 687 | 1,916 | +179% | 0 | 0 | — |
case-06 | pass→pass | 6,666 | 5,832 | -13% | 1 | 1 | 0% | 1,111 | 1,936 | +74% | 0 | 0 | — |
case-07 | pass→pass | 2,696 | 3,250 | +21% | 1 | 1 | 0% | 449 | 1,860 | +314% | 0 | 0 | — |
case-08 | pass→pass | 2,415 | 2,582 | +7% | 1 | 1 | 0% | 404 | 1,750 | +333% | 0 | 0 | — |
case-09 | fail→pass | 2,786 | 2,302 | -17% | 1 | 1 | 0% | 397 | 1,699 | +328% | 0 | 0 | — |
case-10 | fail→fail | 3,336 | 3,059 | -8% | 1 | 1 | 0% | 607 | 1,928 | +218% | 0 | 0 | — |
case-11 | pass→pass | 2,867 | 2,994 | +4% | 1 | 1 | 0% | 508 | 1,845 | +263% | 0 | 0 | — |
case-12 | pass→pass | 3,300 | 3,160 | -4% | 1 | 1 | 0% | 468 | 1,835 | +292% | 0 | 0 | — |
case-13 | pass→pass | 8,104 | 2,982 | -63% | 1 | 1 | 0% | 1,518 | 1,891 | +25% | 0 | 0 | — |
case-14 | pass→pass | 4,100 | 4,205 | +3% | 1 | 1 | 0% | 778 | 2,116 | +172% | 0 | 0 | — |
case-15 | pass→pass | 2,567 | 2,862 | +11% | 1 | 1 | 0% | 418 | 1,770 | +323% | 0 | 0 | — |
case-16 | pass→pass | 2,890 | 2,736 | -5% | 1 | 1 | 0% | 426 | 1,744 | +309% | 0 | 0 | — |
case-17 | pass→pass | 2,960 | 2,628 | -11% | 1 | 1 | 0% | 527 | 1,745 | +231% | 0 | 0 | — |
case-18 | pass→pass | 2,326 | 2,694 | +16% | 1 | 1 | 0% | 380 | 1,719 | +352% | 0 | 0 | — |
case-19 | pass→pass | 3,801 | 2,974 | -22% | 1 | 1 | 0% | 666 | 1,865 | +180% | 0 | 0 | — |
case-20 | pass→pass | 9,125 | 7,075 | -22% | 1 | 1 | 0% | 1,557 | 2,608 | +68% | 0 | 0 | — |
case-21 | pass→pass | 5,549 | 6,400 | +15% | 1 | 1 | 0% | 1,000 | 2,472 | +147% | 0 | 0 | — |
case-22 | pass→pass | 5,145 | 5,219 | +1% | 1 | 1 | 0% | 841 | 2,283 | +171% | 0 | 0 | — |
case-23 | pass→pass | 4,307 | 2,983 | -31% | 1 | 1 | 0% | 759 | 1,842 | +143% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.