Testing Strategy
100% code coverage is a starting point, not a finish line. A test suite can achieve full coverage while still missing critical bugs. Pithos uses a multi-layered testing strategy where each level catches bugs that the others miss.
The Problem with Coverage Aloneโ
Consider this function. It looks simple, but a single test can achieve 100% line coverage while completely missing a critical edge case:
function divide(a: number, b: number): number {
return a / b;
}
A single test achieves 100% coverage:
it("divides two numbers", () => {
expect(divide(10, 2)).toBe(5); // โ
100% coverage
});
But what happens with divide(10, 0)? The test passes, coverage is complete, yet the function silently returns Infinity. Coverage tells you code executed, not that it works correctly.
The Four Levelsโ
Each level of testing answers a different question:
| Level | Prefix | Question Answered | What It Catches |
|---|---|---|---|
| Standard | (none) | "Does each line execute?" | Dead code, forgotten branches |
| Edge case | `[๐ฏ]` | "Are all API contracts tested?" | Untested union types, undocumented behaviors |
| Mutation | `[๐พ]` | "If someone changes this code, will a test fail?" | Weak assertions, silent regressions |
| Property-based | `[๐ฒ]` | "Does it work for _any_ valid input?" | Edge cases: `null`, `""`, `NaN`, huge arrays |
Why This Order Mattersโ
- Standard tests establish that your code runs
- Edge case tests ensure your API promises are kept
- Mutation tests verify your tests are actually checking something
- Property-based tests explore the input space you didn't think of
A Complete Example: evolveโ
The evolve function applies transformation functions to object properties. Here's how each testing level contributes:
// evolve({ a: 5 }, { a: x => x * 2 }) โ { a: 10 }
Standard Tests - "Does it work?"โ
These tests verify the documented behavior:
it("applies transformation functions to properties", () => {
const result = evolve({ a: 5, b: 10 }, { a: (x: number) => x * 2 });
expect(result).toEqual({ a: 10, b: 10 });
});
it("handles nested transformations", () => {
const result = evolve(
{ nested: { value: 5 } },
{ nested: { value: (x: number) => x + 1 } }
);
expect(result).toEqual({ nested: { value: 6 } });
});
it("preserves properties without transformations", () => {
const result = evolve({ a: 1, b: 2 }, {});
expect(result).toEqual({ a: 1, b: 2 });
});
At this point, coverage might be 100%. But are the tests solid?
Edge Case Tests - "Is the API contract complete?"โ
The evolve function accepts transformations as either:
- A function that transforms the whole value
- An object of nested transformations
Both branches must be tested explicitly:
it("[๐ฏ] applies function transformation to object value", () => {
// Tests the "transformation is a function" branch for object values
const result = evolve(
{ nested: { value: 5 } },
{ nested: (obj: { value: number }) => ({ value: obj.value * 2 }) }
);
expect(result).toEqual({ nested: { value: 10 } });
});
The [๐ฏ] prefix signals: "This test covers a specific API branch or documented behavior."
Mutation Tests - "Are my tests actually checking?"โ
Stryker modifies your code and checks if tests fail. If they don't, you have a surviving mutant: a bug your tests would miss.
// Stryker might change this:
if (transformation === undefined) { ... }
// To this:
if (transformation !== undefined) { ... } // Mutant!
If no test fails, you need a targeted test:
it("[๐พ] handles undefined transformation for nested object", () => {
const result = evolve(
{ nested: { value: 5 }, other: 10 },
{ other: (x: number) => x * 2 }
);
// This specifically tests the branch where transformation is undefined
expect(result).toEqual({ nested: { value: 5 }, other: 20 });
});
The [๐พ] prefix signals: "This test exists to kill a specific mutant."
Property-Based Tests - "Does it work for any input?"โ
Instead of testing specific values, test invariants that should hold for all inputs:
import { it as itProp } from "@fast-check/vitest";
import { safeObject } from "_internal/test/arbitraries";
// Invariant: without transformations, object is preserved
itProp.prop([safeObject()])(
"[๐ฒ] preserves object when no transformations",
(obj) => {
expect(evolve(obj, {})).toEqual(obj);
}
);
// Invariant: all keys are preserved
itProp.prop([safeObject()])("[๐ฒ] preserves all keys", (obj) => {
const result = evolve(obj, {});
expect(Object.keys(result).sort()).toEqual(Object.keys(obj).sort());
});
// Independence: evolve returns a new object
itProp.prop([safeObject()])("[๐ฒ] returns new object reference", (obj) => {
const result = evolve(obj, {});
if (Object.keys(obj).length > 0) {
expect(result).not.toBe(obj);
}
});
The [๐ฒ] prefix signals: "This test uses random inputs to verify an invariant."
Property-based tests found bugs in Pithos that hundreds of manual tests missed, particularly around edge cases like empty objects, objects with Symbol keys, and deeply nested structures.
The Resultโ
Running the tests shows all levels working together:

Each test has a clear purpose. No redundancy, no gaps.
When to Use Each Levelโ
| Situation | Recommended Approach |
|---|---|
| New function | Start with standard tests, add property-based for invariants |
| Union types in API | Add `[๐ฏ]` tests for each branch |
| Mutation score < 100% | Add targeted `[๐พ]` tests for surviving mutants |
| Complex input domain | Add `[๐ฒ]` tests with appropriate arbitraries |
| Bug report | Write a failing test first, then fix |
Toolsโ
Running Testsโ
Pithos provides several test commands depending on what you want to verify, from a quick full run to targeted mutation testing on a single file:
# Run all tests
pnpm test
# Run tests with coverage report
pnpm coverage
# Run mutation tests (entire project)
pnpm test:mutation
# Run mutation tests on a specific file
pnpm stryker run --mutate 'packages/pithos/src/arkhe/object/evolve.ts'
Coverage Goalsโ
| Metric | Target | Why |
|---|---|---|
| Code coverage | 100% | Baseline: ensures all code executes |
| Mutation score | 100% | Ensures tests actually verify behavior |
| Property-based tests | 1-5 per function | Explores edge cases automatically |
| Edge case coverage | All union types + `@note` | Ensures API contracts are tested |
The Philosophyโ
"A test that passes when the code is wrong is worse than no test at all: it gives false confidence."
Each prefix tells a story:
- No prefix: "This is expected behavior"
[๐ฏ]: "This covers a specific API branch"[๐พ]: "This kills a specific mutant"[๐ฒ]: "This verifies an invariant holds for any input"
When you see a failing test, the prefix immediately tells you why that test exists and what kind of bug you're dealing with.
Related
- Error Handling โ How Pithos handles errors at the design level
- Best Practices โ Validate at boundaries, trust the types