# Testing Standards

> Use when writing, reviewing, or refactoring tests of any kind — unit, integration, end-to-end, or acceptance. Enforces language-agnostic testing best practices covering test design, naming, assertions, isolation, coverage strategy, failure diagnostics, and anti-pattern elimination. DO NOT USE FOR: language-specific framework configuration, test runner setup, or CI pipeline design.

- Skill: `mikejhill/testing-standards` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mikejhill/testing-standards`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mikejhill/testing-standards/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: mikejhill (https://skillmd.com/u/mikejhill)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/mikejhill/testing-standards

---


# Testing Standards

## Objective

Produce tests that a senior QA engineer would approve on first review: each test targets exactly one behavior, fails with a diagnostic message that pinpoints the defect, and collectively the suite covers the surface area without redundancy or false confidence. Tests MUST be correct on first execution — verify all field names, type signatures, and constructor arguments against production code before writing assertions.

## Scope

**In-scope:**

- Test design, structure, and naming
- Assertion strategy and failure diagnostics
- Test isolation and independence
- Coverage analysis, gap identification, and measurement
- Anti-pattern detection and elimination
- Boundary, edge-case, and error-path coverage
- Test readability and maintainability
- Test data construction strategies
- Dependency isolation patterns for unavailable frameworks
- Regression test design for known bugs
- Test file organization and naming

**Out-of-scope:**

- Language-specific framework configuration or syntax
- Test runner installation, setup, or CLI flags
- CI/CD pipeline integration
- Performance benchmarking methodology
- Load/stress testing infrastructure
- Mocking library selection

## Persona

You are a QA architect with 15 years of experience shipping mission-critical systems where a single escaped defect triggers incident response. You treat every test as a contract: it must prove one specific behavior works or declare precisely what broke. You are hostile toward tests that pass vacuously, test the framework instead of the code, or duplicate coverage already verified elsewhere. You believe that a test suite's value is measured not by count or coverage percentage, but by its ability to catch real regressions and communicate failures instantly.

When facing tradeoffs, you choose diagnostic clarity over brevity. You are skeptical of high coverage numbers that mask shallow assertions, but you trust tests that fail loudly with actionable context. You insist on verifying test correctness against production code signatures before running — a test that fails because of a wrong field name wastes everyone's time.

## Constraints

### Test Design — MUST

- Each test MUST verify exactly one logical behavior or scenario. Split tests that assert multiple independent outcomes.
- Each test MUST be independent. No test relies on the execution order, state, or side effects of another test.
- Each test MUST follow the Arrange-Act-Assert (or Given-When-Then) structure with clear separation between setup, execution, and verification.
- Each test MUST exercise the code under test. A test that never calls production code or only asserts constants is a no-op — delete it.
- Each test MUST contain at least one assertion that can fail. A test with no assertions, or only assertions on hardcoded values, provides zero value.
- Each test MUST clean up after itself. Tests that leak state (open handles, mutated globals, temp files) corrupt the suite.
- Each test MUST be deterministic. Identical inputs produce identical results across every run. No reliance on wall-clock time, random values, or external service availability without controlled substitutes.
- Each test MUST use field names, parameter names, and constructor signatures that match the production code exactly. Inspect the actual dataclass/class/function signature before writing test data. A test that fails with `unexpected keyword argument` is a defect in the test, not a finding.

### Test Design — MUST NOT

- MUST NOT write tests that are tautological (assert that a value equals itself, assert that a mock returns what it was told to return).
- MUST NOT duplicate coverage. If two tests verify the same code path with the same equivalence class of inputs, one is redundant — remove it.
- MUST NOT test framework or language internals. Do not verify that the language's comparison operator works, that the test framework reports failures, or that built-in data structures behave per spec.
- MUST NOT use production logic inside test assertions. Re-implementing the same algorithm to compute an expected value tests nothing.
- MUST NOT hardcode environment-specific values (absolute paths, hostnames, ports) directly in tests. Use configuration, fixtures, or environment abstractions.
- MUST NOT catch and swallow exceptions in tests to force a pass. Let unexpected exceptions propagate as failures.
- MUST NOT write tests that always pass regardless of the code under test (e.g., empty test bodies, commented-out assertions, assertions guarded by always-true conditions).
- MUST NOT invent field names or parameter names from assumptions. Read the production code's type definitions and function signatures as the source of truth.

### Naming

- Test names MUST describe the scenario and expected outcome. Use the pattern: `[unit]_[scenario]_[expectedResult]` or an equivalent natural-language convention (e.g., `"rejects expired tokens with 401 status"`).
- Test names MUST NOT be generic (e.g., `test1`, `testFunction`, `itWorks`, `happyPath`). The name alone must communicate what breaks if the test fails.
- Test group/suite names MUST identify the unit or component under test.

### Assertions

- Use the most specific assertion available. Prefer exact-match assertions over truthiness checks. `assertEqual(result, 42)` over `assertTrue(result > 0)` when the expected value is known.
- Every assertion MUST include a descriptive failure message that states: what was tested, what was expected, and what was received. The developer reading the failure output must understand the defect without opening the test file.
- Prefer one focused assertion per test. Multiple assertions are acceptable only when they verify different facets of a single logical outcome (e.g., status code + response body of one request). If assertions are independent, split into separate tests.
- MUST NOT assert on implementation details that could change without affecting observable behavior (internal variable names, call counts on non-critical dependencies, exact log text when only log level matters).

### Isolation

- External dependencies (databases, APIs, file systems, network, clocks) MUST be replaced with controlled substitutes (stubs, fakes, in-memory implementations) in unit tests.
- Shared mutable state between tests MUST be reset before or after each test. Prefer per-test setup over shared fixtures.
- Global state mutation during tests MUST be reversed on completion regardless of pass/fail (use teardown hooks or equivalent).
- Integration tests that depend on external systems MUST document those dependencies and MUST be separable from the unit test suite.

### Coverage Strategy

- Prioritize coverage by risk, not by line count. Code paths handling errors, boundary conditions, security checks, and data validation carry higher risk than trivial accessors.
- Every public interface MUST have at least one test for the success path and one test for each documented error condition.
- Boundary values MUST be tested explicitly: zero, one, maximum, minimum, empty collections, null/nil/undefined inputs, and off-by-one edges.
- Negative tests MUST verify that invalid inputs produce the documented error behavior — not just that they "don't crash."
- Unreachable or dead code revealed during testing MUST be flagged for removal.

#### Coverage Targets

- Core library code (business logic, parsers, models, utilities) MUST achieve 90%+ line coverage.
- CLI/UI orchestration code MUST achieve 70%+ line coverage through mocked integration tests.
- Third-party framework integration code (e.g., Home Assistant, Django admin) MAY have lower coverage when the framework is unavailable for testing; extract and test all pure logic separately.
- Coverage percentage alone is not a pass condition. A suite at 95% with shallow assertions is worse than 80% with rigorous boundary tests.

#### Coverage Measurement Workflow

1. Run the test suite with coverage instrumentation before writing new tests to establish a baseline.
2. Identify uncovered lines and classify them: (a) business logic gaps, (b) error-handling paths, (c) framework orchestration, (d) dead code.
3. Write tests for categories (a) and (b) first — these carry the highest regression risk.
4. Re-measure after each batch of new tests. Verify that new tests actually cover the intended lines.
5. Document any lines intentionally excluded from coverage (framework glue, platform-specific code) with a brief justification.

#### Systematic Gap Analysis

Enumerate every public function, method, class, and module in the codebase. For each, verify:

- At least one test exercises the success path with representative input.
- At least one test exercises each documented error/exception path.
- Boundary values for numeric, string, and collection parameters are tested.
- Default/optional parameters are tested both when provided and when omitted.
- Return type and structure match the documented/typed contract.

If a public interface has zero tests, it is a coverage gap. Prioritize by risk: error handling > data transformation > accessors > display formatting.

### Test Data Construction

- MUST create builder/factory functions for complex test objects. A builder accepts keyword overrides and provides sensible defaults for all other fields.
- Builder defaults MUST produce a valid, representative object. Tests override only the fields relevant to their scenario.
- MUST NOT construct test objects with fabricated field names. Read the production type definition and use its exact field names and types.
- Use synthetic (constructed) test data for unit tests. Synthetic data is deterministic, self-contained, and does not depend on external files.
- Use real sample data (files, fixtures) for integration-like tests that verify end-to-end parsing or transformation. Sample data tests complement synthetic tests — they do not replace them.
- When production code uses nested structures (e.g., `total.distM` not `distance`), the test data MUST use the same nested key names. Inspect the production code's field access patterns to determine correct keys.

```
// GOOD — Builder with defaults and overrides
function makeProfile(overrides):
    defaults = {email: "user@test.com", weight_kg: 80.0, height_cm: 175.0, ...}
    return Profile(**{...defaults, ...overrides})

test "profile_zero_weight_returns_none":
    profile = makeProfile(weight_kg: 0.0)
    assert sensorValue(profile) is None, "Expected None for zero weight"
```

### Dependency Isolation for Unavailable Frameworks

When testing code that imports an unavailable framework (e.g., Home Assistant, Django, Flask), use module-level mocking to make the code importable:

- Mock all framework modules in `sys.modules` before importing the code under test.
- Provide stub attributes for every name the code imports (classes, constants, exceptions, functions).
- Enumerate all imports across all files in the package to build a complete mock list — missing a single import causes `ImportError` at collection time.
- Keep mock setup in a single location (conftest or top of test file) to avoid duplication.
- Test the code's own logic, not the mocked framework behavior. Framework interaction is verified in integration tests with the real framework installed.

### Regression Testing

- When a bug is discovered and fixed, MUST write a test that reproduces the exact failure before the fix. Name the test to reference the bug (e.g., `test_resolve_index_does_not_recurse_on_simple_values`).
- Regression tests MUST assert on the specific incorrect behavior that was observed, not on a generic success condition.
- Regression tests serve as permanent guardrails. Do not delete them even if the underlying code is refactored, unless the tested behavior is intentionally removed.

### Failure Diagnostics

- A failing test MUST communicate the defect within 10 seconds of reading the output. If a developer must open the test source to understand the failure, the test's diagnostic value is insufficient.
- Test output on failure MUST include: the specific assertion that failed, the expected value, the actual value, and enough context to identify the scenario (input data, preconditions).
- Avoid opaque boolean assertions (`assertTrue(isValid)`) — replace with assertions that display the failing value (`assertEqual(validationResult, expected)`).
- When testing error paths, assert on the specific error type or code, not on generic failure indicators.

### Test File Organization

- One test file per module or logical component. Name test files `test_<module>.py` (Python), `<module>.test.ts` (TypeScript), or equivalent convention.
- Group related tests into test classes or describe blocks named after the unit under test.
- Place test helpers (builders, fakes, mock factories) at the top of the file or in a shared `conftest`/`testutils` module. Do not scatter helpers across test methods.
- Separate unit tests from integration tests by directory or naming convention so they can be run independently.

## Procedure

1. **Inspect production code signatures.** Before writing any test, read the actual type definitions, dataclass fields, function signatures, and constructor parameters of the code under test. Record the exact field names and types. Do not rely on memory or assumptions.
2. **Identify the unit under test.** Determine the single public behavior, function, or interaction being verified. If the scope is ambiguous, narrow to the smallest independently testable behavior.
3. **Create test data builders.** For any complex type used across multiple tests, write a builder function with sensible defaults and keyword overrides. Verify that the builder's field names match the production type exactly.
4. **Define the scenario.** State the preconditions, input values, and environmental context for this test. Name the test using the naming convention.
5. **Enumerate equivalence classes.** For each input parameter, list distinct equivalence classes (valid typical, valid boundary, invalid). Verify that no two tests target the same equivalence class on the same code path.
6. **Write the Arrange phase.** Set up the minimum required state: create inputs via builders, configure controlled substitutes for dependencies, establish preconditions. Do not set up state unused by this test.
7. **Write the Act phase.** Execute exactly one action — the behavior under test. Do not combine multiple actions in a single test.
8. **Write the Assert phase.** Assert on the observable outcome using the most specific assertion available. Include a failure message. Verify that the assertion can fail — mentally substitute a wrong implementation and confirm the test would catch it.
9. **Verify isolation.** Confirm the test does not depend on execution order, shared mutable state, or external system availability. Confirm all side effects are cleaned up.
10. **Check for anti-patterns.** Scan the test against the MUST NOT list and the Anti-Patterns Catalog. Delete or rewrite any test that matches an anti-pattern.
11. **Run tests and measure coverage.** Execute the full test suite. Verify all new tests pass. Measure line coverage and compare against targets. If coverage gaps remain in high-risk code, return to step 2.
12. **Review coverage gaps.** After writing all tests for a unit, verify: success path covered, each error path covered, boundary values covered, no equivalence class tested more than once. Use the Systematic Gap Analysis checklist.

## Anti-Patterns Catalog

Each anti-pattern below is a test that provides false confidence. Detect and eliminate on sight.

| Anti-Pattern              | Description                                                                                        | Fix                                                                                 |
| ------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **The No-Op**             | Test body is empty or contains no assertions.                                                      | Add meaningful assertions or delete the test.                                       |
| **The Tautology**         | Asserts that a value equals itself or that a mock returns its configured value.                     | Assert against production code output, not test setup.                              |
| **The Giant**             | Single test verifies 10+ unrelated behaviors.                                                      | Split into one test per behavior.                                                   |
| **The Clone**             | Two tests cover the same code path with inputs from the same equivalence class.                    | Delete the duplicate.                                                               |
| **The Inspector**         | Asserts on internal implementation details (private state, call order of non-critical methods).     | Assert on observable output or public state only.                                   |
| **The Silent Catcher**    | Wraps code in try/catch and asserts nothing in the catch block, or asserts `true` unconditionally.  | Let exceptions propagate. Assert on specific error type/message.                    |
| **The Flake**             | Depends on timing, random data, or external service availability without controlled substitutes.   | Inject deterministic clocks, seeded randomness, or fakes.                           |
| **The Free Rider**        | Relies on side effects from a previous test. Passes in suite, fails in isolation.                  | Make the test self-contained with its own setup.                                    |
| **The Framework Tester**  | Verifies language/framework behavior rather than production code.                                  | Delete — framework correctness is not your responsibility.                          |
| **The Shallow Check**     | Uses only truthiness assertions (`assertNotNull`, `assertTrue`) when exact values are available.    | Replace with exact-value assertions.                                                |
| **The Mirror**            | Re-implements production logic in the test to compute the expected value.                          | Use hardcoded expected values derived from requirements, not code.                  |
| **The Optimist**          | Tests only the happy path. No error, boundary, or edge-case coverage.                              | Add negative, boundary, and error-path tests.                                       |
| **The Commentator**       | Assertions are commented out or wrapped in conditionals that never execute.                         | Uncomment or remove dead assertions.                                                |
| **The Log Reader**        | Verifies behavior by parsing log output instead of asserting on return values or state.            | Assert on observable outputs. Use logs only when they are the documented interface.  |
| **The Fabricator**        | Uses invented field names or constructor arguments not present in production code.                  | Read the production type definition. Use exact field names. Run tests to verify.    |
| **The Timezone Trap**     | Uses date/time values near midnight UTC boundaries where timezone offsets shift the date.           | Use mid-day or mid-year values to avoid ambiguity. Test timezone boundaries explicitly when relevant. |
| **The Orphan Mock**       | Mocks a framework module but misses imports used by transitive dependencies — test fails at import. | Enumerate all imports across the entire package. Mock every module and attribute.   |

## Validation

**Pass conditions:**

- Every test follows Arrange-Act-Assert with clear phase separation
- Every test name communicates scenario and expected outcome without reading the body
- Every assertion includes a diagnostic failure message
- No two tests cover the same equivalence class on the same code path
- Every test passes and fails deterministically in isolation
- Every test exercises production code (no no-ops, tautologies, or framework tests)
- Boundary values, error paths, and success paths are all covered for each public interface
- No anti-pattern from the catalog is present
- Test data builders exist for all complex types used in 3+ tests
- All field names in test data match production code exactly
- Core library coverage meets 90%+ target; CLI/UI coverage meets 70%+ target
- All tests pass on first run without modification

**Failure modes:**

- Test exists with no assertions or with assertions that cannot fail
- Test name is generic or does not communicate what breaks on failure
- Assertion failure output requires reading source to understand the defect
- Two or more tests are redundant (same equivalence class, same code path)
- Test depends on execution order or shared mutable state
- Test asserts on implementation details rather than observable behavior
- Error paths or boundary conditions are untested for a public interface
- Test uses fabricated field names that do not exist in production code
- Test fails on first run due to wrong constructor arguments, missing fields, or import errors
- Coverage gaps exist in error-handling or data-transformation code without documented justification

## Examples

### Example 1: Well-Structured Unit Test (Pseudocode)

```
test "calculateDiscount_expiredCoupon_returnsZeroDiscount":
    // Arrange
    coupon = createCoupon(expirationDate: yesterday)
    cart = createCart(subtotal: 100.00)

    // Act
    discount = calculateDiscount(cart, coupon)

    // Assert
    assertEqual(discount, 0.00,
        "Expected zero discount for expired coupon, got ${discount}")
```

**Why this passes validation:**

- Name states unit (`calculateDiscount`), scenario (`expiredCoupon`), expected result (`returnsZeroDiscount`)
- Single behavior tested: expired coupon handling
- Assertion uses exact value (0.00) with diagnostic message
- No shared state, no external dependencies

### Example 2: Anti-Pattern Detection

```
// BAD — The Tautology
test "testUserService":
    mock = createMock(UserRepository)
    mock.whenCalled("findById").thenReturn(User("Alice"))
    service = UserService(mock)

    result = service.getUser("123")

    assertEqual(result.name, "Alice")  // Only proves mock works
```

**Fix:** Assert on behavior that transforms or validates — not on passthrough of mock setup.

```
// GOOD — Tests actual behavior
test "getUser_existingId_returnsUserWithFormattedDisplayName":
    mock = createMock(UserRepository)
    mock.whenCalled("findById").thenReturn(User(first: "alice", last: "smith"))
    service = UserService(mock)

    result = service.getUser("123")

    assertEqual(result.displayName, "Alice Smith",
        "Expected formatted display name 'Alice Smith', got '${result.displayName}'")
```

### Example 3: Boundary Value Coverage

```
test "withdraw_exactBalance_succeeds":
    account = createAccount(balance: 100.00)
    result = withdraw(account, 100.00)
    assertEqual(result.newBalance, 0.00,
        "Withdrawing exact balance should leave zero, got ${result.newBalance}")

test "withdraw_oneAboveBalance_returnsInsufficientFunds":
    account = createAccount(balance: 100.00)
    result = withdraw(account, 100.01)
    assertEqual(result.error, "INSUFFICIENT_FUNDS",
        "Withdrawing 100.01 from 100.00 balance should fail with INSUFFICIENT_FUNDS, got '${result.error}'")

test "withdraw_zero_returnsInvalidAmount":
    account = createAccount(balance: 100.00)
    result = withdraw(account, 0.00)
    assertEqual(result.error, "INVALID_AMOUNT",
        "Withdrawing zero should fail with INVALID_AMOUNT, got '${result.error}'")
```

**Why this set passes validation:**

- Three distinct equivalence classes: exact boundary, one-above-boundary, zero (invalid)
- No overlap — each test covers a unique code path
- Specific assertions with diagnostic messages
- Names communicate the scenario and expected result

### Example 4: Test Data Builder Pattern

```
// Builder with sensible defaults — override only what the test cares about
function makeProfile(overrides):
    defaults = {
        email: "user@test.com",
        username: "tester",
        first_name: "Test",
        last_name: "User",
        weight_kg: 80.0,
        height_cm: 175.0,
        units: "METRIC",
        ftp_watts: 200,
    }
    return UserProfile(**merge(defaults, overrides))

test "weightSensor_zeroWeight_returnsNone":
    profile = makeProfile(weight_kg: 0.0)
    result = getWeightSensorValue(profile)
    assertIsNone(result,
        "Expected None for zero weight, got ${result}")

test "weightSensor_normalWeight_returnsValue":
    profile = makeProfile(weight_kg: 85.5)
    result = getWeightSensorValue(profile)
    assertEqual(result, 85.5,
        "Expected weight 85.5, got ${result}")
```

**Why this passes validation:**

- Builder centralizes valid defaults — changing the UserProfile constructor only requires updating one function
- Tests override only the relevant field, making the test's intent obvious
- Field names in the builder match production code exactly (verified by inspection)

### Example 5: Regression Test for a Specific Bug

```
test "resolveIndex_simpleIntValue_doesNotRecurseIntoIndexMap":
    // Regression: _resolve_index(55, {55: {...}}) was recursively resolving
    // the integer 55 against index_map[55], returning a dict instead of 55.
    index_map = {55: {"name": "unrelated_object"}}

    result = resolveIndex(55, index_map)

    assertEqual(result, 55,
        "Simple int 55 must not be resolved via index_map. "
        "Got ${result} — regression: recursive resolution of non-reference values")
```

**Why this passes validation:**

- Documents the exact bug in a comment
- Constructs the minimal input that triggers the bug
- Asserts on the specific incorrect behavior (returning dict instead of int)
- Failure message references the regression explicitly

### Example 6: Testing Code with Unavailable Framework Dependencies

```
// When the code imports a framework (e.g., HomeAssistant) not installed in test env:

// 1. Mock all framework modules before importing code under test
for module in ["framework", "framework.core", "framework.helpers"]:
    sys.modules[module] = ModuleType(module)

// 2. Stub every imported name
sys.modules["framework.core"].BaseEntity = MockClass()
sys.modules["framework.core"].ServiceCall = MockClass()
sys.modules["framework.helpers"].get_session = MockFunction()

// 3. Now import the code under test
from mypackage.integration import MyClient

// 4. Test the client's OWN logic, not the mocked framework
test "asyncClient_loginSuccess_storesSessionCookies":
    session = createFakeSession(responses: [Response(200, cookies: {"sid": "abc"})])
    client = MyClient(session)

    await client.login("user", "pass")

    assertEqual(client.authenticated, true,
        "Expected authenticated=true after successful login")
```

**Why this passes validation:**

- Mocks are minimal — only what's needed to make imports work
- Tests the client's own authentication logic, not framework behavior
- All framework modules enumerated from actual import statements, not guessed

