| name | Testing Patterns |
| description | This skill should be used when the user asks to "write tests", "test strategy", "coverage", "unit test", "integration test", or needs testing guidance. Provides testing methodology and patterns. |
| version | 2.1.0 |
Provide testing patterns and strategies for comprehensive test coverage and maintainable test suites.
Current standard for JS/TS testing; preferred for all new projects
Stable Browser Mode, visual regression testing, snapshot testing, ESM-native, Vite-powered
Widely used JS/TS test runner; still supported but Vitest is preferred for new projects
Linting for JS/TS codebases; flat config only (eslintrc format removed)
Property-based testing for Haskell
Property-based testing with integrated shrinking for Haskell
Property-based testing for JavaScript/TypeScript
Test individual functions/methods in isolation
Single function, class, or module
Fast, isolated, deterministic
Business logic, utility functions, transformations
Test interaction between components
Multiple components working together
Slower, may use real dependencies
API endpoints, database operations, service interactions
Test complete user workflows
Full application stack
Slowest, tests real user scenarios
Critical user journeys, smoke tests
Percentage of code lines executed during tests
Measures which lines of code are exercised
Percentage of code branches (if/else, switch) taken during tests
More thorough than line coverage as it measures decision paths
Percentage of functions/methods called during tests
Identifies untested functions
Coverage measured against ISO/IEC 25010 quality characteristics, not only executed lines or branches
Check each characteristic for applicability: functional suitability, performance efficiency, compatibility, usability, reliability, security, maintainability, portability
Three-phase test structure for clear test organization
Are you writing unit or integration tests?
Apply arrange-act-assert pattern for clear test structure
Consider given-when-then for BDD-style tests
Arrange: Set up test data and preconditions
user = User.new(name: "John")
cart = ShoppingCart.new(user)
<test_phase>Act: Execute the code under test</test_phase>
total = cart.calculate_total
<test_phase>Assert: Verify expected outcomes</test_phase>
assert_equal 0, total
Separates setup, execution, and verification into distinct phases
BDD-style test structure focusing on behavior
Is the test focused on business behavior rather than technical implementation?
Apply given-when-then pattern for BDD-style tests
Use arrange-act-assert for technical unit tests
Given: Initial context (preconditions)
given_a_user_with_an_empty_cart
<bdd_step>When: Action or trigger</bdd_step>
when_the_user_calculates_total
<bdd_step>Then: Expected outcome</bdd_step>
then_the_total_should_be_zero
Emphasizes business behavior over technical implementation
Provide canned responses for dependencies
Does the test need dependency responses but not interaction verification?
Apply stub pattern for canned responses
Use mock if interaction verification is needed
api_client = stub(
fetch_user: { id: 1, name: "John" }
)
Replace slow/unreliable dependencies
Verify interactions occurred with dependencies
Does the test need to verify specific interactions occurred?
Apply mock pattern to verify method calls and arguments
Use stub if only canned responses are needed
email_service = mock()
email_service.expect(:send_email, args: ["user@example.com", "Welcome"])
user_service.register(email_service)
email_service.verify
Ensure methods called with correct arguments
Record calls while using real implementation
Does the test need real behavior plus interaction verification?
Apply spy pattern to record calls while using real implementation
Use stub for canned responses or mock for behavior replacement
logger = spy(Logger.new)
service.process(logger)
assert_called logger, :log, with: "Processing complete"
Verify side effects without changing behavior
Working implementation suitable for testing
Does the test need a simplified but working implementation?
Apply fake pattern for lightweight working implementation
Use stub for simple canned responses
class FakeDatabase
def initialize
@data = {}
end
def save(key, value)
@data[key] = value
end
def find(key)
@data[key]
end
end
In-memory database, fake file system
Test names that clearly describe scenario and outcome
Is this a technical unit test for a specific method?
Apply descriptive naming with method-scenario-result format
Consider should naming for BDD-style tests
test_calculateTotal_withEmptyCart_returnsZero
test_calculateTotal_withMultipleItems_returnsSumOfPrices
test_calculateTotal_withDiscount_appliesDiscountCorrectly
Format: test_[method]_[scenario]_[expected_result]
BDD-style naming that reads like natural language
Is this a behavior-focused test readable by non-technical stakeholders?
Apply should naming for natural language readability
Use descriptive naming for technical unit tests
calculateTotal_should_returnZero_when_cartIsEmpty
calculateTotal_should_applyDiscount_when_couponIsValid
calculateTotal_should_throwError_when_pricesAreNegative
Format: [method]_should_[expected_behavior]_when_[condition]
Generate random inputs to verify properties that should always hold
Does the function have a property or invariant that holds for all valid inputs?
Apply property-based testing to generate random inputs and verify invariants
Use example-based tests with arrange-act-assert
Haskell (QuickCheck/Hedgehog)
prop_reverse_involutive xs = reverse (reverse xs) == xs
<note>JS/TS (fast-check)</note>
fc.assert(
fc.property(fc.array(fc.integer()), (arr) =>
deepEqual(arr, reverse(reverse(arr)))
)
)
QuickCheck (Haskell), Hedgehog (Haskell), fast-check (JS/TS), Hypothesis (Python)
Serialization round-trips, sorting invariants, mathematical properties, parser correctness
Capture output and compare against a stored reference snapshot
Is the output complex and best verified by comparing against a known-good reference?
Apply snapshot testing to detect unintended output changes
Use explicit assertions for specific values
expect(renderComponent()).toMatchSnapshot()
Vitest 4.x supports visual regression testing via Browser Mode for comparing rendered UI screenshots against baseline images
Component rendering, serialized data structures, CLI output
Design test cases by rotating through adversarial reviewer perspectives so perspective-coverage gaps surface systematically
Are you designing test cases and need to avoid missing an entire class of scenarios?
Rotate through every adversarial perspective; each must contribute at least one check
Use a single focused pattern (arrange-act-assert) for a known, isolated scenario
V1 naive user: intuitive misuse, unexpected operation order
V2 heavy user: rapid, bulk, or sustained input; behavior under load
V3 adversarial input: boundary values, invalid values, out-of-permission operations, injection
V4 integrity auditor: verify persisted state directly, not the return value or UI alone
V5 compatibility/migration: existing data, legacy formats, missing or malformed data
V6 regression sentinel: side effects on neighboring features; existing behavior preserved
V7 spec skeptic: divergence from the primary source (requirements, spec, source code)
Rule: each perspective leaves at least one confirmation point; never trust "it should work"
Test-case design reviews, coverage-gap detection, exhaustive scenario enumeration
Wait for an asynchronous outcome by polling a definitive source at a fixed interval until a terminal condition or timeout, instead of asserting-and-retrying via thrown exceptions
Does the test depend on work that completes asynchronously (a job status, a persisted record, a downstream side effect)?
Poll a completion helper with an explicit interval and timeout budget, both expressed in one unit
Assert directly on the synchronous return value
Poll the authoritative store until the terminal status appears, or fail once at timeout
await waitForStatus(store, id, "COMPLETED", { intervalMs: 200, timeoutMs: 30000 })
Prefer a single polling helper over throw-to-retry with exponential backoff: the retry-on-exception form re-runs assertion machinery on every attempt, hides the actual terminal state behind the last exception, and couples total wait to backoff math rather than a declared budget
Express interval and timeout in one explicit unit (for example milliseconds as numbers); mixing string durations with numeric durations invites silent coercion at the framework boundary
End-to-end pipeline tests, eventual-consistency checks, queue or worker completion
Drive many related scenarios from one typed array of case records rather than copy-pasting near-identical test bodies
Are several tests identical except for input and expected classification (for example a family of boundary and invalid-input checks)?
Define a typed case record (id, name, input, expected, optional skip) and iterate, tagging each assertion with the case id
Write a single explicit test
Each record carries a stable id so a failure names the exact case
interface Case { id: string; name: string; input: string; expect: Status; skip?: { reason: string } }
for (const c of cases) {
if (c.skip) continue // record intent; do not silently drop
assert(run(c.input), `${c.id}: ${c.name}`).hasStatus(c.expect)
}
Carry a per-case skip reason in the record instead of commenting cases out, so intentionally-unrun cases stay visible in the table and a failure message pinpoints the offending row
Boundary-value matrices, validation-error families, cross-input contract checks
When an external dependency required by an integration or E2E test is not reachable, skip the test with a stated reason rather than letting it fail
Does the test require an out-of-process dependency (database, message queue, emulated cloud service, remote endpoint) that may be absent in some environments?
Probe availability in a setup hook; on absence, skip with a reason so the signal reads "not exercised", not "broken"
Run unconditionally
Setup hook gates the scenario on a reachability probe
setup: async (ctx) => { if (!(await dependencyReachable())) ctx.skip("dependency unavailable in this environment") }
A skipped test and a failed test carry different meanings: reserve failure for a violated expectation about code under your control, and use skip for a missing precondition of the environment. Conflating the two trains readers to ignore red
Integration suites that run both locally and in CI, optional emulator-backed paths
Give each scenario its own unique identifiers for the data it creates, and clean up by those identifiers, instead of truncating shared tables between tests
Do tests write to a shared persistent store that other tests, or parallel workers, also use?
Generate a unique id per scenario, tag created records with it, and delete by that id in teardown
Local in-memory state can be reset wholesale in teardown
Unique per-scenario key isolates cleanup and enables parallel runs
const runId = `test-${scenario}-${uuid()}`
// write records tagged with runId; teardown deletes where tag = runId
Truncating shared tables is a blunt reset that breaks the moment tests run concurrently, and it can destroy seed or fixture data the suite did not create. Scenario-scoped ids keep cleanup surgical and order-independent
Database- or queue-backed E2E suites, parallel execution against shared infrastructure
A shared fixture that snapshots every global or persistent binding it touches, runs the body, and restores the originals under an unwind guarantee
Does a test mutate global state (a registry, a dynamic variable, a function binding, a hash table)?
Wrap the mutation in a fixture that saves the prior value, runs the body, and restores it in an unwind-protected cleanup, covering the full set of state the body can touch
Keep the test purely local
Restore must run even on non-local exit; cover every binding the body mutates
with-restored (place-a place-b ...) ; snapshot each, run body, unwind-protect the restore
The classic pollution bug is a fixture that restores one binding but not a second one the same code path also mutates (for example a lookup table populated as a side effect of loading a mode or style). A later test then inherits the leaked entries. Enumerate the complete write set of the body and snapshot all of it, not just the obvious binding
For several globals, build a thin multi-binding wrapper over the single-binding helper rather than nesting many restore forms; accept both bare bindings and generalized places (such as a function cell) so simple and complex cases share one abstraction
Registry mutations, dynamic-variable overrides, environment-variable tests, hash-table caches
When a macro generates globally-registered test names from a caller-supplied label, namespace the label so names cannot collide across files
Does a table- or case-generating macro derive globally-registered test names from a bare label?
Prefix the label with the module or subject, because the enclosing describe or context block is usually not part of the generated name
No action needed for inline, locally-named tests
Module-prefixed label avoids a silent collision with an identically-named table elsewhere
deftest-table server-strip-annotation-cases ... // not: strip-annotation-cases
Two generators sharing a derived name across files can silently shadow or overwrite each other's registrations, so one suite's cases quietly vanish with no failure. Uniqueness must hold across the whole suite, not just within a file
Data-driven test generators, table or case macros that register names globally
A mock or fake must implement the full observable contract of what it replaces, including destructive or ordering semantics, not only the return value
Are you replacing a dependency whose real behavior has side effects beyond returning a value (it deletes a region, consumes input, mutates a buffer, removes a key)?
Reproduce those side effects in the double, or the test passes against behavior that cannot occur in production
A value-only stub is sufficient
The real call deletes its input range before writing output; the double must too
(lambda (beg end &rest _) (delete-region beg end) (insert "output") 0)
A double that only inserts leaves stale input in place and hides the bug
Under-modeled doubles are a common source of tests that are green yet meaningless: they assert against a fiction. When the contract includes deletion, consumption, or ordering, the double owns those semantics
Process and IO shims, store deletion semantics, buffer-mutating calls
Model assertion outcomes as structured failure values (or conditions), not bare booleans, so a failure reports what was expected, what was observed, and where
Are you building assertion or matcher infrastructure rather than a single test?
Emit a structured outcome (expected, actual, location, message) and let the runner render or convert it; where the host language has a condition system, signal a typed failure and expose named restarts
Use the framework's existing assertions
A structured outcome explains itself; a boolean cannot
fail({ expected, actual, at: location, message })
In a language with conditions and restarts, signal a typed failure and install continue, skip, and retry restarts around the body
A runner that converts typed failures to ordinary events when no outer handler intervenes stays deterministic in CI while still letting an interactive handler inspect the live condition before conversion. A retry restart that reruns the attempt without consuming the configured retry budget, and cleanup placed under an unwind guarantee, keep this control flow predictable. Boolean assertions discard that structure and force the reader back to the source to reconstruct intent
Custom matchers, assertion libraries, runners with retry, skip, or interactive-restart support
Keep each matcher a single deterministic transformation from actual value to verdict; compose matchers rather than embedding branching or side effects in one
Are you authoring a matcher or custom assertion?
Make it one pure step with no hidden state or ordering dependence, and build complex checks by composing simple matchers
Not applicable
Deterministic single step: the same input always yields the same verdict and message
hasStatus(expected) => actual => actual.status === expected ? pass() : fail({ expected, actual: actual.status })
Matchers that branch on external state or mutate as a side effect become order-dependent and hard to reason about; a matcher should be a referentially transparent verdict function
Matcher libraries, fluent assertion DSLs
<best_practices>
Test happy path first
Start with the normal, expected flow before edge cases
test_userLogin_withValidCredentials_succeeds
test_userLogin_withInvalidPassword_fails
test_userLogin_withLockedAccount_fails
Test edge cases
Test boundary conditions and limits
Empty inputs, maximum values, null values, zero values, negative numbers
Test error cases
Verify error handling paths work correctly
Invalid inputs, network failures, permission errors, timeout scenarios
Ground expected values in evidence
Base every expected value on a primary source (requirements, spec, or source code); when a value cannot be verified, mark it explicitly instead of guessing
Good: expectation traceable to its basis
test_calculateTotal_appliesTenPercentDiscount_perSpecSection4
<note>Unverifiable: state it, do not fabricate</note>
expected total: unverified -- requires-analysis
</example>
Isolate tests
Each test should be independent
Use setup/teardown to reset state
def setup
@database = TestDatabase.new
@service = UserService.new(@database)
end
def teardown
@database.clear
end
</example>
Make tests readable
Tests serve as documentation
Good: Clear and descriptive
test_userRegistration_withExistingEmail_returnsError
<note>Bad: Unclear purpose</note>
test_user_reg_1
</example>
One assertion per concept
Each test should verify one logical concept
Good: Single concept
test_userCreation_setsDefaultRole
user = create_user
assert_equal "member", user.role
end
<note>Avoid: Multiple unrelated assertions</note>
test_userCreation
user = create_user
assert_equal "member", user.role
assert_not_nil user.email
assert_true user.active
end
</example>
Use test fixtures and factories
Extract common test data setup
Create reusable test data
def create_test_user(overrides = {})
defaults = {
name: "Test User",
email: "test@example.com",
role: "member"
}
User.new(defaults.merge(overrides))
end
Avoid magic numbers
Use named constants for test values
Good
VALID_USER_AGE = 25
MINIMUM_AGE = 18
test_userValidation_withValidAge_succeeds
user = User.new(age: VALID_USER_AGE)
assert user.valid?
end
<bad_example>Bad</bad_example>
test_userValidation_withValidAge_succeeds
user = User.new(age: 25)
assert user.valid?
end
</example>
Test corner cases
Test unusual combinations and scenarios
Concurrent access, timezone edge cases, leap years, DST transitions
<anti_patterns>
Testing implementation details instead of behavior
Focus on testing observable behavior and outcomes, not internal implementation details. Test what the code does, not how it does it.
Over-mocking dependencies throughout test suites
Use real implementations where practical; excessive mocking often indicates poor design. Only mock external dependencies or slow operations.
Tests that sometimes pass and sometimes fail
Ensure tests are deterministic by controlling time, randomness, and async operations. Use fixed timestamps, seeded random generators, and proper async handling.
Tests that take too long to run
Use unit tests for fast feedback; reserve slow integration/e2e tests for critical paths. Unit tests should run in milliseconds, not seconds.
Tests that depend on execution order or shared state
Make each test independent with proper setup/teardown and isolated state. Each test should create its own test data.
Filling in expected values by guessing when they cannot be verified against a source
State unknowns explicitly (mark as unverified or requires-analysis) and cite the primary source for known expectations. Do not fabricate assertions to make a test look complete.
Using AI merely to run tests while neglecting exhaustive test-case design and perspective coverage
Use AI as a test designer first: enumerate scenarios across adversarial perspectives before execution. Design coverage, then run.
Waiting for an async outcome by repeatedly asserting and catching the failure until it eventually passes
Poll a definitive completion source at a fixed interval with an explicit timeout budget; reserve the exception for the final timeout, not for every intermediate attempt.
Resetting a shared persistent store by truncating tables between tests
Tag created data with a scenario-unique id and delete by that id, so cleanup is surgical, order-independent, and safe under parallel execution.
Letting a test fail when a required external dependency is simply unavailable in the current environment
Probe availability and skip with a stated reason; keep failure meaning "expectation violated", not "precondition absent".
A fixture that restores some but not all of the global state its body mutates
Enumerate the complete write set of the body and snapshot/restore every binding under an unwind guarantee; a single leaked binding pollutes later tests.
A mock or fake that returns the right value but omits the real dependency's side effects such as deletion, consumption, or ordering
Implement the full observable contract in the double, or the test verifies behavior that cannot occur in production.
<tooling_traps>
Environment- and toolchain-level failure modes that silently invalidate otherwise well-designed tests. These are not test-design mistakes; they are traps in how the runner, compiler, or server is wired, and each can make a green result meaningless or a red result misleading.
A source edit appears to have no effect, or a test keeps failing or passing against behavior that no longer matches the code
A previously compiled output sits beside the source and the resolver picks it up first. A bundler-based runner resolving an extensionless relative import tries the compiled .js before the .ts by default, so a stale .js next to an edited .ts is exercised instead of the new source
Regenerate or delete the stale artifact, or import the intended file with an explicit extension, before trusting a runner result. When a change "does nothing", suspect a shadowing artifact before suspecting the test
Default bundler resolution order places .js ahead of .ts for extensionless imports, so identically-named siblings resolve to the compiled file first
A focused run of a few test files fails a coverage threshold even though the files under test are fully covered
A global coverage threshold is evaluated over the aggregate of only the files measured in that run. A subset run measures a distorted slice, because files imported but not exercised register as uncovered, so the aggregate can fall below a gate the full suite would satisfy
Run the full suite before trusting a global gate, or configure per-file thresholds so each measured file is judged on its own coverage rather than on a distorted global average. Change global coverage exclusions only deliberately
Global thresholds check the aggregate of measured files, so subset runs can fail gates the whole suite would pass unless per-file thresholds are used
A browser E2E test that expects a mock-backed response gets real or stale responses instead, and only in local runs
The runner is configured to reuse an already-running server on the target URL, a common local-only setting. When any independently-started server occupies that port, the runner reuses it and never launches the mock-backed command the test assumes, so the test exercises the wrong backend
Ensure no unrelated server holds the port before a mock-dependent run, or disable server reuse for suites whose correctness depends on the launched command; keep reuse enabled only where it is safe. In CI, start fresh so a stale process cannot make the suite pass for the wrong reason
Server-reuse settings reuse an independently-running server on the URL and skip the configured start command; the typical guard enables reuse only outside CI
Aim for high coverage but prioritize meaningful tests over coverage numbers
80%+ coverage is a good target for critical code paths
100% coverage does not guarantee bug-free code
Focus on testing behavior, not achieving coverage metrics
Prefer project conventions over generic defaults
<error_escalation inherits="core-patterns#error_escalation">
Minor coverage gap in non-critical path
Test flakiness detected
Critical path lacks test coverage
Tests reveal security vulnerability
</error_escalation>
<related_agents>
Locate relevant code patterns
Review output consistency
</related_agents>
Follow project test patterns
Run tests after creation
Cover critical paths first
Creating tests without understanding implementation
Writing flaky or non-deterministic tests
Ignoring existing test conventions
<related_skills>
Use to define test requirements and acceptance criteria
Use to implement tests as part of feature development workflow
Use when debugging test failures or flaky tests
</related_skills>