Imported from cartaorobbin/pyramid-grpc (
.claude/skills/write-pytest/SKILL.md). Install upstream withnpx skills add cartaorobbin/pyramid-grpc --skill write-pytest. Copyright stays with the author.
MANAGED SKILL — Do not edit this file here. It is managed from an external source repository. To make changes, update the source and run "update my skills" to sync.
Write Pytest Tests
Write pytest tests for any Python project following a conversational, phased approach. Always research first, discuss with the user, then write. Defer framework-specific patterns (Pyramid test infrastructure, web client wiring) to the matching helper skill — this skill stays framework-agnostic.
Core Workflow
- Research the project (read-only): source under test, dependencies, framework signals, existing test infra.
- Discuss the proposed test cases, factories, fixtures with the user — one question at a time.
- Write the tests following the constraints below.
- Verify by running pytest; fix failures and re-run until green.
Constraints
MUST DO
- Read the source under test before proposing any test case.
- Detect the framework in Phase 1 (Pyramid, Flask, Django, plain library) and route to the right helper for framework specifics.
- Detect security signals (auth headers, decorators, middleware, ACLs) in Phase 1. If any are present, security tests are mandatory — never ask whether to include them.
- Use plain pytest functions, descriptive names, and a brief docstring per test.
- Place all imports at module top.
- Extract reusable setup into
@pytest.fixturein the nearestconftest.py. - Use
@pytest.mark.parametrize(withids=) when tests share logic but differ in input/output. - Prefer real code paths and
responsesfor HTTP. Reach forunittest.mock.patchonly when the dependency cannot be exercised any other way. - Keep test files under ~200 lines. Split by concern when they grow (
_api,_service,_integration,_security).
MUST NOT DO
- Never use FastAPI or pydantic in examples or generated code.
- Never use class-based pytest tests (e.g.,
class TestFoo: def test_method(self): ...). The single exception ishypothesis.stateful.RuleBasedStateMachine— it is hypothesis's stateful API, exposed to pytest via itsTestCaseattribute, not a pytest test class. See invariants.md. - Never write conditional logic inside test functions (
if,else,for,while,try, ternary). - Never put imports inside functions or methods.
- Never create standalone helper functions for setup — use fixtures.
- Never mock when
responsesor a real integration would work. - Never skip security tests when the endpoint under test is protected.
- Never edit test files inside
~/.cursor/skills/or~/.claude/skills/— those are installed copies.
Reference Guide
Load these supplementary files only when the listed signals appear. Inline content in this SKILL covers the common case.
| Topic | Reference | Load When |
|---|---|---|
Property invariants and stateful state machines (hypothesis) |
invariants.md | Code under test contains validators, pure transformations, numeric calculations, parsers, or stateful systems whose correctness is best expressed as invariants |
Pyramid test infrastructure (testapp, dbsession, tm, factory wiring, plugins, xdist) |
skills/pyramid-app/pyramid-tests.md | Project shows Pyramid signals: pyramid_tm, Cornice Service(), __acl__, pyramid.scripting.prepare, webtest.TestApp, testing.postgresql |
Setup Question (Asked at Install Time)
During installation, use AskQuestion:
title: "Companion Rule"
questions:
- id: install_rule
prompt: "Do you want to install a Cursor rule that enforces pytest patterns whenever test files are created or modified?"
options:
- id: "yes"
label: "Yes, install the test-patterns rule"
- id: "no"
label: "No, just the skill"
If yes, dual-write templates/test-patterns-rule.mdc as test-patterns (.cursor/rules/test-patterns.mdc and .claude/rules/test-patterns.md) in the target project.
Phase 1: Research the Project (Read-Only)
Gather context before proposing anything. Do NOT write code yet.
1.1 Read the source code
Read the file(s) the user wants tested. Identify the type of code:
- Web endpoint / view — Cornice
Service, Flask route, Django view, or any HTTP-handling callable - Service / business logic — domain classes or functions
- Model — ORM model definitions (SQLAlchemy, Django ORM, etc.)
- Async task — Celery task, RQ job, asyncio coroutine
- Pure utility — validators, parsers, calculators, transformers
- Stateful component — caches, queues, state machines, in-memory stores
1.2 Detect the framework
Look for these signals to choose the right test approach (and the right reference file):
| Signal | Framework |
|---|---|
pyramid_tm, Cornice Service(), pyramid.scripting.prepare, webtest.TestApp, __acl__ in context factories |
Pyramid — load skills/pyramid-app/pyramid-tests.md |
flask.Flask, app.test_client(), flask.Blueprint |
Flask |
django.urls, django.test.TestCase, pytest-django, settings module |
Django |
| None of the above | Plain Python library — generic patterns only |
There are no dedicated test-infrastructure helpers in this repo for Flask or Django yet. When the framework is Flask, Django, or another web framework not listed above, apply the generic patterns in this skill and adapt the test-client API to the framework's conventions:
- Flask —
flask.testing.FlaskClient: useclient.get(url, headers=...)/client.post(url, json=payload). Status codes are read fromresponse.status_code. Noexpect_errorsflag needed. - Django —
django.test.Client: useclient.get(url, **headers)/client.post(url, data=payload, content_type="application/json"). Status codes come fromresponse.status_code. - Plain library — no HTTP client; security tests are skipped (no auth surface). Focus on correctness, edge cases, and invariants.
1.3 Map dependencies
For each source file, identify:
- External HTTP services (
requests.get/post,httpx, rawurllib) - gRPC clients (client/stub imports and calls)
- Database access (ORM queries, raw SQL, sessions)
- Internal services or modules the code calls
1.4 Detect security requirements
Look for any of these signals — if found, security tests are mandatory:
- Authentication decorators or middleware
- Permission/authorization checks (
permission=,@login_required, ACL__acl__, role checks) - JWT, session cookies, API keys, or auth header parsing
- Context factories with
factory=(Pyramid)
1.5 Check existing test infrastructure
Read these files if they exist:
- Root
tests/conftest.pyand any subdirectoryconftest.py— available fixtures tests/factories.pyortests/factories/— existing factory-boy factories- Existing test files for similar modules — naming and style conventions
- The size of any existing test file you would append to (relevant for the ~200-line cap)
1.6 Detect reuse opportunities
- Repeated setup logic that should become a fixture
- Validation / pure-function code that should be tested with property invariants — load invariants.md
- Stateful components (caches, queues, in-memory stores) that should be tested with
RuleBasedStateMachine— load invariants.md
1.7 Build draft test case list
Group proposed cases by category:
- Happy path — success scenarios
- Validation errors — missing/invalid fields, bad input
- Security (if any auth signal was found) — mandatory, see Phase 3 — Security Tests
- Edge cases — boundary conditions, empty data, large payloads
- Integration — external service calls, response handling
- Property invariants — see invariants.md
- Stateful invariants — see invariants.md
Phase 2: Discuss with the User
Use AskQuestion one question at a time. Never skip this phase.
Q1 — Review proposed test cases
Present the draft test case list from Phase 1, grouped by category:
title: "Proposed Test Cases"
questions:
- id: test_cases
prompt: "<present the grouped list here with brief descriptions>"
options:
- id: accept
label: "Looks good, proceed"
- id: revise
label: "I want to adjust the list"
If revise, collect the user's changes before continuing.
Q2 — Confirm factory needs (if applicable)
If new factory-boy factories or updates to existing ones are needed:
title: "Factory-Boy Factories"
questions:
- id: factories
prompt: "<list new/updated factories needed>"
options:
- id: accept
label: "Create/update these factories"
- id: revise
label: "I want to adjust"
Skip if no new factories are needed.
Q3 — Confirm fixture needs (if applicable)
If new conftest.py fixtures are needed or existing setup should be extracted:
title: "Pytest Fixtures"
questions:
- id: fixtures
prompt: "<list new fixtures or extractions proposed>"
options:
- id: accept
label: "Create/update these fixtures"
- id: revise
label: "I want to adjust"
Skip if no new fixtures are needed.
Phase 3: Write the Tests
Generate code following all of these rules. Constraints from the top of this file always apply.
Structure
- Test file mirrors source structure under
tests/. - One test file per module:
test_<module_name>.py. - Descriptive function names:
test_<action>_<scenario>ortest_<what>_<expected_outcome>. - Brief docstring on each test stating what is being tested.
- Keep files under ~200 lines; split by concern when growing (
_api,_service,_integration,_security).
Testing priority (strictly enforced)
- Real integrations and real code paths — always prefer exercising actual code.
responsesfor external HTTP calls (@responses.activate).factory-boyfor test data generation.- Pytest fixtures for reusable setup.
unittest.mock.patchonly as last resort (e.g., gRPC clients, hardware drivers).
Mandatory security tests
If Phase 1 detected ANY security signal, include ALL of the following — they are non-negotiable. The examples below use a generic client fixture; adapt the call shape (get / post(json=...), error handling, header helpers) to the project's test client. For Pyramid specifics — webtest.TestApp semantics, expect_errors=True, auth_headers, dbsession wiring — see skills/pyramid-app/pyramid-tests.md.
- Wrong/invalid token — request with malformed or expired token, expect 401 or 403.
- Wrong permission scope — request with valid token but insufficient/wrong permissions, expect 403.
- Missing authentication — request with no auth headers at all, expect 401 or 403.
- Wrong credentials — when the endpoint accepts credentials (e.g., login), test with wrong password/username, expect 401.
The exact status code (401 vs 403) depends on the framework's auth chain; the asserts below use tuple membership (in (401, 403)) for the cases where either is acceptable. Tuple membership is not conditional logic — it is a single boolean expression — so it remains compliant with the no-conditional-logic rule.
def test_endpoint_rejects_invalid_token(client, dbsession):
"""Verify endpoint rejects requests with an invalid token."""
resource = ResourceFactory()
headers = {"Authorization": "Bearer invalid-token-here"}
response = client.get(
f"/api/v1/resource/{resource.uuid}",
headers=headers,
)
assert response.status_code in (401, 403)
def test_endpoint_rejects_wrong_permission(client, dbsession, auth_headers):
"""Verify endpoint rejects requests with insufficient permissions."""
resource = ResourceFactory()
headers = auth_headers(["wrong::permission::scope"])
response = client.get(
f"/api/v1/resource/{resource.uuid}",
headers=headers,
)
assert response.status_code == 403
def test_endpoint_rejects_missing_auth(client, dbsession):
"""Verify endpoint rejects unauthenticated requests."""
resource = ResourceFactory()
response = client.get(f"/api/v1/resource/{resource.uuid}")
assert response.status_code in (401, 403)
pytest.mark.parametrize to avoid duplication
When multiple tests share the same logic but differ only in input/expected output:
@pytest.mark.parametrize(
"payload,missing_field",
[
({"name": "John Doe", "email": "john@example.com"}, "tax_id"),
({"tax_id": "12345678901", "email": "john@example.com"}, "name"),
({"tax_id": "12345678901", "name": "John Doe"}, "email"),
],
ids=["missing_tax_id", "missing_name", "missing_email"],
)
def test_create_person_rejects_missing_field(client, payload, missing_field):
"""Verify endpoint rejects payload missing a required field."""
response = client.post("/api/v1/person", json=payload)
assert response.status_code == 400
Do NOT use parametrize when the test logic itself differs between cases.
Property and stateful invariants with hypothesis
When the code under test is a validator, parser, numeric calculation, data transformation, or a stateful component, propose hypothesis-based tests. The full guidance — strategies, properties to look for, and RuleBasedStateMachine patterns — lives in invariants.md. Read it during Phase 1 if any of those signals appear.
Quick reach for property invariants:
from hypothesis import given, strategies as st
@given(value=st.floats(allow_nan=False, allow_infinity=False))
def test_score_normalization_stays_in_range(value):
"""Verify score normalization always produces a value in [0, 1]."""
result = normalize_score(value)
assert 0.0 <= result <= 1.0
For stateful systems (caches, queues, in-memory stores) load invariants.md and follow the RuleBasedStateMachine recipe.
Reusable setup must be pytest fixtures
- Extract repeated setup into
@pytest.fixtureinconftest.py. - Place fixtures in the nearest
conftest.py(directory-level for local, root for project-wide). - Fixtures can compose other fixtures and factories.
@pytest.fixture
def verified_person(dbsession):
"""A person that has passed KYC verification."""
person = PersonFactory(status="verified")
dbsession.flush()
return person
Factory-boy patterns
Pick the base class that fits the project:
factory.Factory— generic, no ORMfactory.alchemy.SQLAlchemyModelFactory— SQLAlchemy projects (Pyramid, Flask-SQLAlchemy, plain SQLAlchemy)factory.django.DjangoModelFactory— Django projects
class CompanyFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = Company
sqlalchemy_session_persistence = "flush"
registration_number = factory.LazyFunction(
lambda: f"{random.randint(10000000000000, 99999999999999)}"
)
name = factory.Faker("company")
revenue = factory.Faker("random_int", min=100000, max=10000000)
created_at = factory.Faker("date_time")
- Use
factory.Fakerfor realistic data. - Use
factory.SubFactoryfor relationships. - Use
factory.LazyFunctionfor computed fields.
For Pyramid + SQLAlchemy session/transaction wiring, see skills/pyramid-app/pyramid-tests.md.
Integration test patterns
External HTTP — use responses:
@responses.activate
def test_kyc_service_validates_person():
"""Verify KYC service handles person validation response."""
responses.add(
responses.POST,
"https://api.kyc-provider.com/v1/validate",
json={"valid": True, "status": "verified", "confidence": 0.95},
status=200,
)
service = KYCService()
result = service.validate_person(person_data)
assert result["valid"] is True
assert result["status"] == "verified"
gRPC clients — use patch as last resort:
def test_legal_entity_grpc_integration():
"""Verify legal entity gRPC client handles validation response."""
with patch("module.clients.legal_entity.LegalEntityClient") as mock_client:
mock_client.return_value.validate_company.return_value = {
"valid": True,
"company_status": "active",
}
service = LegalEntityService()
result = service.validate_company("12345678000190")
assert result["valid"] is True
Phase 4: Verify
- Run
pytest <test_file> -vto confirm all tests pass. - If hypothesis tests were added, run once with
--hypothesis-show-statisticsto confirm strategies generate meaningful examples. - Report results to the user: which tests passed, which failed, any issues.
- If tests fail, fix and re-run until green.
Error Recovery
pytest collects no tests:
- Confirm the file matches the discovery pattern (
test_*.py) and lives under a path covered bytestpathsinpyproject.toml/pytest.ini. - Confirm test functions start with
test_. - If using parametrize, confirm
ids=does not collide with another test's id.
Fixture not found:
- Confirm the fixture is declared in a
conftest.pyat or above the test file. - A subdirectory
conftest.pydoes not expose fixtures to siblings — move it up if shared. - For framework-specific fixtures (
testapp,dbsession,tm), follow skills/pyramid-app/pyramid-tests.md.
Hypothesis flaky / Flaky exception:
- A reproducible counter-example was lost between runs. Pin it with
@example(...)so it is always tried. - If the failure depends on time or random state outside hypothesis, narrow the strategy or move that input to a fixture.
Hypothesis HealthCheck failure (filtering / data generation too slow):
- Replace
assume(...)filters with a more constrained strategy (st.integers(min_value=..., max_value=...),st.from_regex). - For a single legitimate case, suppress with
@settings(suppress_health_check=[HealthCheck.filter_too_much])and document why.
Hypothesis DeadlineExceeded:
- Increase
@settings(deadline=...)only when the slowness is intrinsic (e.g., DB-backed test). For pure logic, fix the slow path instead of widening the deadline.
Factory writes not visible to the code under test:
- Confirm
sqlalchemy_session_persistence = "flush"(not"commit"). - For Pyramid: confirm
extra_environwirestm.managerandapp.dbsession— see skills/pyramid-app/pyramid-tests.md.
Test file exceeds ~200 lines:
- Split by concern:
test_<module>_api.py,test_<module>_service.py,test_<module>_integration.py,test_<module>_security.py. - Propose the split to the user before moving tests.
responses raises ConnectionError on an unmatched call:
- The code is hitting an URL not registered with
responses.add(...). Either register the URL or, if the call is incidental, useresponses.add_passthru(...)for the prefix that should bypass mocking.