skills/technical-patterns/policyengine-test-writing-skill/SKILL.md
This skill should be used when writing unit tests, integration tests, or test fixtures for PolicyEngine frontend apps, APIs, SDKs, and standalone tools. NOT for country model packages (policyengine-us, policyengine-uk, etc.) — those use YAML-based tests with their own conventions. Covers the Given-When-Then naming convention, fixture extraction, edge case coverage, and the rule that only modified test files should be run. Triggers: "write tests", "add tests", "unit test", "test file", "test coverage", "write a test for", "test this function", "test this component", "given when then", "test fixtures", "mock setup", "edge cases", "test naming", "test convention"
npx skillsauth add policyengine/policyengine-claude policyengine-test-writingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Standard conventions for writing tests in PolicyEngine frontend apps, APIs, SDKs, and standalone tools. These rules apply to every language and framework (Vitest, pytest, etc.) unless a project-specific override exists.
Do NOT apply this skill to country model packages (policyengine-us, policyengine-uk,
policyengine-canada, etc.). Those repos use YAML-based tests with entirely different structure,
naming, and tooling. For country packages, use these instead:
policyengine-testing-patterns-skill (skills/technical-patterns/policyengine-testing-patterns-skill/SKILL.md) — YAML test structure, naming conventions (variable_name.yaml, integration.yaml), period handling, error margins, and quality standardstest-creator agent (agents/country-models/test-creator.md) — Automated agent that creates comprehensive YAML integration tests for government benefit program implementationsCountry model tests are .yaml files that live alongside the variables they test, not .test.ts or
.test.py files in a separate tests/ directory.
Every test name follows the pattern test__given_X_condition__then_Y_occurs:
// TypeScript / Vitest
test("test__given_valid_income__then_tax_is_calculated", () => { ... });
test("test__given_negative_income__then_error_is_thrown", () => { ... });
test("test__given_zero_children__then_ctc_is_zero", () => { ... });
# Python / pytest
def test__given_valid_income__then_tax_is_calculated():
...
def test__given_negative_income__then_error_is_thrown():
...
Inside the test body, organize code into three clearly commented sections:
test("test__given_user_clicks_submit__then_form_is_submitted", async () => {
// Given
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<Form onSubmit={onSubmit} />);
// When
await user.click(screen.getByRole("button", { name: /submit/i }));
// Then
expect(onSubmit).toHaveBeenCalledOnce();
});
Each source file gets exactly one corresponding test file named test_FILENAME:
| Source file | Test file |
|---|---|
| utils/formatCurrency.ts | tests/unit/utils/test_formatCurrency.test.ts |
| components/MetricCard.tsx | tests/unit/components/test_MetricCard.test.tsx |
| lib/api/client.ts | tests/unit/lib/api/test_client.test.ts |
| services/simulation.py | tests/unit/services/test_simulation.py |
The test file mirrors the source directory structure under a tests/ root.
All mocks, setup code, patches, constants, and test data must be extracted to a fixture file with the same name in a fixtures/ directory:
tests/
├── fixtures/
│ ├── utils/
│ │ └── test_formatCurrency.ts ← mocks, constants, helpers
│ ├── components/
│ │ └── test_MetricCard.ts
│ └── lib/
│ └── api/
│ └── test_client.ts
├── unit/
│ ├── utils/
│ │ └── test_formatCurrency.test.ts ← imports from fixtures
│ ├── components/
│ │ └── test_MetricCard.test.tsx
│ └── lib/
│ └── api/
│ └── test_client.test.ts
What goes in fixtures:
vi.fn() / MagicMock setup helpersbeforeEach / afterEach setup functionsWhat stays in the test file:
describe / test blocksexpect / assert statementsImport everything from the fixture:
import {
VALID_HOUSEHOLD,
EMPTY_HOUSEHOLD,
mockApiSuccess,
mockApiError,
EXPECTED_TAX_AMOUNT,
} from "@/tests/fixtures/lib/api/test_client";
Every test file must cover, at minimum:
Structure the describe block to make coverage obvious:
describe("calculateTax", () => {
// Happy path
test("test__given_valid_income__then_correct_tax_returned", () => { ... });
test("test__given_income_at_bracket_boundary__then_correct_bracket_applied", () => { ... });
// Edge cases
test("test__given_zero_income__then_zero_tax", () => { ... });
test("test__given_negative_income__then_throws_error", () => { ... });
// Error handling
test("test__given_api_timeout__then_error_propagated", () => { ... });
test("test__given_malformed_response__then_fallback_used", () => { ... });
});
After writing or modifying test files, run only those specific tests — never the entire suite:
# TypeScript / Vitest — run specific test file(s)
bunx vitest run tests/unit/utils/test_formatCurrency.test.ts
# Python / pytest — run specific test file(s)
pytest tests/unit/variables/test_income.py -v
After tests pass, run formatters and typecheckers only on modified files:
# TypeScript — typecheck and lint only changed files
bunx tsc --noEmit
bunx eslint tests/unit/utils/test_formatCurrency.test.ts tests/fixtures/utils/test_formatCurrency.ts
# Python — format and lint only changed files
black tests/unit/variables/test_income.py tests/fixtures/variables/test_income.py
ruff check tests/unit/variables/test_income.py tests/fixtures/variables/test_income.py
Never run the full test suite or full linter unless explicitly asked. Large codebases take minutes to lint/test; running everything wastes time and produces noise unrelated to the changes.
import { describe, test, expect, vi, beforeEach } from "vitest";
vi.fn() for mocks, vi.mock() for module mocksvi.clearAllMocks() in beforeEachgetByRole, getByLabelText) over test IDsuserEvent.setup() for user interactions (not fireEvent)waitFor for async state updatesimport pytest
from unittest.mock import MagicMock, patch
@pytest.fixture for setup, import from fixture files@pytest.mark.parametrize for data-driven testspytest.raises(ExceptionType) for error assertions@pytest.mark.slowFor fixture best practices, mock patterns, and accessibility selector priority, consult:
references/fixture-patterns.md — Comprehensive fixture organization and mock examplesdevelopment
ALWAYS LOAD THIS SKILL for PolicyEngine PR reviews, including when the user invokes $review-program or Codex /review on a PolicyEngine PR. Performs read-only code validation, source-reference checks, regulatory review, optional PDF audit, summary reporting, and optional GitHub comment posting.
development
Use when the user invokes $fix-pr or asks Codex to apply fixes to a PolicyEngine PR based on $review-program findings, GitHub review comments, CI failures, or local review reports.
development
Use when the user invokes $encode-policy-v2 or asks Codex to implement a new PolicyEngine-US state benefit program from official rules. Covers research, source collection, requirement extraction, scoped implementation, tests, validation, and draft PR preparation.
development
Deploying PolicyEngine frontend apps to Vercel - naming, scope, team settings