| name | unit-testing |
|---|---|
| description | Guide for generating, reviewing, and planning unit tests. Use when writing tests from scratch, improving existing tests, or identifying what to test. Framework-agnostic best practices for writing effective, maintainable unit tests. |
Write tests that verify behavior, not implementation. A good test catches real bugs, reads like documentation, and survives refactoring.
In order of importance:
- Tests the right thing — verifies behavior that matters, not implementation details
- Fails when it should — catches real bugs, not just coverage theater
- Readable — someone can understand what's being tested without reading the implementation
- Maintainable — doesn't break when you refactor internals
Before writing any test, answer:
- What does this function promise? (not what does it do internally)
- What inputs are valid? What happens with invalid ones?
- What can go wrong? How should failures manifest?
- What side effects matter? (if any)
This reframes test generation from "cover the code" to "verify the promises."
Use the AAA pattern:
- Arrange — minimal setup
- Act — one call
- Assert — one logical assertion
Name tests clearly: Method_Scenario_ExpectedBehavior
Examples:
Add_EmptyString_ReturnsZeroLogin_ExpiredToken_ThrowsAuthErrorParseConfig_MissingRequiredField_ReturnsValidationError
Don't overtest. A 2-function class doesn't need 20 tests.
Rules of thumb:
- 1-3 tests per public method for straightforward logic
- +1 test per significant branch (if/else that matters)
- +1 test per error condition that callers need to handle
- Edge cases only for critical inputs — not every possible null/empty/boundary
Ask: "If this test caught a bug, would I care?" If no, skip it.
Coverage theater is worse than no tests — it creates maintenance burden without catching bugs.
Consider these based on input types. Don't exhaust all — focus on important inputs and critical flows:
| Input Type | Edge Cases to Consider |
|---|---|
| String | empty "", whitespace, null/undefined |
| Number | 0, negative, boundary values |
| Array/List | empty [], single element |
| Object/Map | empty {}, missing required keys |
| Optional/Nullable | null, undefined, absent |
The question: "What inputs would break this in production?" — test those.
- Test every valid transition
- Test that invalid transitions fail/throw
- Test initial state and terminal states
- Each rule in isolation
- Rules in combination (do they interact?)
- Boundary values: "just valid" and "just invalid"
- Known input → known output
- Inverse operations:
parse(serialize(x)) === x - Mathematical properties: commutativity, associativity, idempotence
- Private methods — test through the public interface
- Framework code — don't test that React renders or that Express routes
- Trivial getters/setters — unless they have logic
- Third-party libraries — trust them unless user explicitly wants to verify outputs (ask if unclear)
- Implementation details — internal method calls, execution order (unless it's the contract)
Prefer in this order:
- Real — use actual implementations when feasible (fast dependencies, no side effects)
- Fake — lightweight working implementation (in-memory DB, fake payment gateway)
- Mock/Stub — last resort for things you can't control (external APIs, clock, filesystem)
Mock at architectural boundaries only. If mocking your own code, you're probably testing implementation details.
| Smell | Problem |
|---|---|
| Setup > 15 lines | Code under test is too coupled |
| Mocking own code | Testing implementation, not behavior |
| Assertions on mock calls | Testing how, not what |
| Magic numbers without context | Intent unclear |
| Tests depend on order | Shared mutable state |
| Flaky tests | Time-dependent or race conditions |
expect(result).toBe(true) |
What does true mean here? |
| 20 tests for 2 functions | Overtesting, coverage theater |
Avoid these patterns:
- Tautological tests —
expect(add(2,2)).toBe(4)whenaddliterally doesa + b. Proves nothing. - Testing the mock — asserting the mock returned what you told it to return
- Happy path obsession — 5 tests for the golden path, 0 for errors
- Copy-paste structure — same test shape applied blindly to different scenarios
- Missing the contract — testing what code does rather than what it should do
- Read the function signature and docstring
- Identify the CONTRACT (what does it promise?)
- List edge cases by input type (use the table, don't exhaust)
- List failure modes (what can go wrong?)
- For each behavior:
- Write test name:
Method_Scenario_ExpectedBehavior - Arrange: minimal setup
- Act: one call
- Assert: one logical assertion
- Write test name:
- Review: Am I testing behavior or implementation?
List what you're not testing — one line each, with reason:
Skipped: getUsername() (trivial getter), formatDate() (thin wrapper), API error handling (needs mocks — ask if wanted)
Keep it brief. User can ask for more if needed.