Unit Testing Patterns skill for Codex
Tests that survive refactors and fail for one reason. Codex writes tests around the behaviors a unit promises rather than the methods it happens to have. It picks inputs at boundaries, mocks only at real boundaries, injects clocks and seeds, proves each test can fail, and flags design smells such as too many collaborators instead of burying them under mocks.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- unit-testing-patterns Installs to ~/.agents/skills/unit-testing-patterns/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install unit-testing-patterns The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project unit-testing-patterns Installs to .agents/skills/unit-testing-patterns/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/unit-testing-patterns.zipunzip -o unit-testing-patterns.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) unit-testing-patterns PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $unit-testing-patterns, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs unit tests that pin down behavior instead of implementation - arrange-act-assert structure, behavior-first naming, boundary and equivalence cases, test doubles chosen by role, and deterministic time, randomness and IO. Language-neutral with TypeScript, Python and Go examples. Use when writing tests for new or untested code, reviewing a test file, deciding what to mock, or when tests break on every refactor.
Use it when
- Adding tests to code that has none
- Reviewing a test file for brittleness or weak assertions
- Deciding between a stub, a fake and a mock
- Tests depend on the clock, random values or run order
Use something else when
- You want the red-green-refactor loop for new code: use test-driven-development
- You need reusable builders for complex test data: use test-data-factories
How to check the result
Each new test fails when the production code is deliberately broken and passes again when reverted, and the file passes repeatedly under random ordering.
The mistake it prevents
Asserting that internal collaborators were called with exact arguments, which couples the test to the implementation and breaks on every refactor.
A good unit test fails for exactly one reason, names that reason in its title, and keeps passing through any refactor that preserves behavior. If a test breaks when you rename a private helper, it is testing the wrong thing.
Before writing anything
- Read
AGENTS.mdfor the test command, framework and file layout. Match the existing suite’s style (file naming, assertion library, fixtures) before introducing your own. - Open two or three existing test files near the code you are testing. Copy their helpers and setup patterns instead of inventing parallel ones.
- Run the existing tests once and confirm they pass, so any later failure is yours.
- Identify the unit’s public surface: exported functions, class methods callers use, HTTP handler, CLI entry. That surface is what you test.
Workflow
- [ ] 1. List behaviors (not methods) the unit promises
- [ ] 2. For each behavior, pick inputs: typical, boundary, invalid
- [ ] 3. Decide which collaborators need doubles, and which kind
- [ ] 4. Write each test as Arrange / Act / Assert
- [ ] 5. Make the test fail on purpose once to prove it can
- [ ] 6. Remove duplication that hides intent, keep duplication that shows it
- [ ] 7. Run the file, then the whole suite
1. List behaviors
Write one line per rule the code must uphold, in domain language:
- “orders over 100 EUR ship free”
- “an expired token is rejected with 401, not 500”
- “parsing an empty file returns an empty list, not an error”
Each line becomes one or more tests. If you cannot phrase a behavior, read the callers before writing the test; the code may be doing several jobs.
2. Choose inputs deliberately
| Technique | Apply it to | Example for discount(total) with tiers at 100 and 500 |
|---|---|---|
| Equivalence classes | one representative per class | 50, 250, 900 |
| Boundary values | each edge and its neighbours | 99.99, 100, 100.01, 499.99, 500 |
| Invalid input | each way the input can be wrong | -1, NaN, null, “100” |
| Empty / one / many | collections | [], [x], [x, y, z] |
| Special values | strings, numbers, dates | "", unicode, 0, max int, DST change, leap day |
Off-by-one bugs live at boundaries. A tier test with only 50 and 900 proves almost nothing.
3. Choose test doubles by role
| You need to… | Use | Assert on |
|---|---|---|
| Satisfy a parameter that is never used | dummy | nothing |
| Feed canned answers into the unit | stub | the unit’s output |
| Replace a slow or stateful dependency with a working light version | fake (in-memory repo, fake clock) | the unit’s output and the fake’s state |
| Check that an outbound side effect happened (email sent, event published) | spy or mock | the call, once, with key arguments |
Rules:
- Prefer real objects for values and pure logic. Mock only at boundaries: network, disk, clock, randomness, queues, third-party SDKs.
- Do not mock types you do not own. Wrap the SDK in a thin adapter you own and double the adapter; cover the adapter with an integration test.
- Assert on calls only for commands (side effects). Never assert that a query method was called; assert on the result it produced.
- More than three doubles in one test means the unit has too many collaborators. Report that as a design smell rather than piling on mocks.
4. Arrange / Act / Assert
Keep the three phases visible. One Act per test.
it("ships free when the subtotal reaches exactly 100 EUR", () => {
// Arrange
const cart = cartWith({ price: 60 }, { price: 40 });
// Act
const quote = quoteShipping(cart, "DE");
// Assert
expect(quote.cost).toBe(0);
expect(quote.reason).toBe("free-threshold");
});
def test_rejects_expired_token_with_401(fake_clock, token_factory):
token = token_factory(expires_at=fake_clock.now() - timedelta(seconds=1))
response = authenticate(token, clock=fake_clock)
assert response.status == 401
assert response.error == "token_expired"
Multiple asserts are fine when they describe one outcome (status and body of one response). Two Acts in one test means two tests.
5. Prove the test can fail
Break the production code briefly (flip a comparison, return early) or change the expected value, run the test, and watch it fail with a readable message. A test that cannot fail is worse than no test because it reports false safety. Revert the break.
6. DAMP over DRY
- Keep the values that matter to the behavior inside the test body. A reader should not jump to a fixture to learn why 100 is special.
- Hide only incidental setup behind builders or factories (
cartWith(...),token_factory(...)). For larger factory design, hand off to$test-data-factories. - Avoid loops and conditionals inside a test. Use the framework’s parametrization so each case reports separately.
Naming
A test name is a sentence a product owner could read in a failure report.
| Weak | Strong |
|---|---|
test_discount_2 | test_discount_applies_10_percent_from_100_eur |
it("works") | it("returns an empty list for an empty file") |
TestParse | TestParse/rejects_trailing_comma |
should call repo.save | persists the order with status PENDING |
Pattern: <unit> <expected outcome> when <condition>; drop the unit name if the
describe block already states it.
Determinism
A unit test must produce the same result on every machine, in any order, in parallel.
| Source of nondeterminism | Control it by |
|---|---|
| Current time | inject a clock, or framework fake timers; never call Date.now()/time.time() deep in logic |
| Random values, UUIDs | inject a seeded generator or an id factory |
| Timezone, locale | set TZ=UTC in the test command and pass locale explicitly |
| Filesystem | use the framework temp dir (tmp_path, t.TempDir(), os.tmpdir() + cleanup) |
| Network | never in unit tests; stub at the adapter |
| Shared mutable state | build fresh state per test; reset singletons in teardown |
| Test order | run with random ordering where supported (pytest-randomly, go test -shuffle=on, RSpec --order random) |
type Clock interface{ Now() time.Time }
func TestInvoiceIsOverdueAfter30Days(t *testing.T) {
issued := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
clock := fixedClock(issued.AddDate(0, 0, 31))
inv := Invoice{IssuedAt: issued}
if !inv.Overdue(clock) {
t.Fatalf("invoice issued %v should be overdue at %v", issued, clock.Now())
}
}
What to test, what to skip
Test:
- Branching business rules, calculations, parsing, validation, state transitions.
- Error paths: each distinct error the caller can observe.
- Bugs you fixed: one regression test per bug, named after the behavior, not the ticket.
Skip or test elsewhere:
- Trivial getters, framework wiring, generated code: covered by integration tests.
- Private helpers directly: test them through the public function that uses them. If a helper is complex enough to need its own tests, it wants to be its own unit.
- Exact log strings and exact SQL text, unless the format is the contract.
Test smells to fix during review
- Mirror test: re-implements the production algorithm to compute the expected value. Use a literal expected value worked out by hand.
- Over-specified mock: verifies every call and argument; breaks on harmless refactors.
- Sleep:
sleep(100)to wait for async work. Await the promise or use fake timers. - Mystery guest: depends on a file, env var or database row the test does not create.
- Assertion-free test: runs code and checks nothing, or only
not.toThrow()where a real outcome exists. - Catch-all try/except inside the test that turns failures into passes.
- Snapshot of a large object standing in for three real assertions.
Verification
- Run the new tests alone, then the full unit suite, using the command from
AGENTS.mdor the package manifest. - Run the new file three times, and with random order if the runner supports it, to catch hidden order dependence.
- Confirm each new test failed at least once during step 5.
- If a coverage tool is configured, check that the branches you targeted are now covered; do not chase the percentage for its own sake.
Output
When reporting back, list:
**Tests added:** tests/pricing/test_discount.py (7 cases)
**Behaviors covered:** tier boundaries at 100 and 500, negative totals rejected,
currency rounding to cents
**Doubles used:** FakeClock (time), no mocks
**Not covered:** tax rules in tax.py (needs a design change; collaborators are global)
**Command:** uv run pytest tests/pricing -q -> 7 passed What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Unit Testing Patterns"
short_description: "Tests that survive refactors and fail for one reason"
default_prompt: "$unit-testing-patterns Write unit tests for the discount calculator in pricing.ts" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.