# Test Lens

> Judge test value and choose what to test or mock. Use for brittle tests, test reviews, and unit or integration test design; use tdd for the test-first execution loop.

- Skill: `robsonrung/test-lens` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add robsonrung/test-lens`
- Raw SKILL.md: https://api.skillmd.com/api/skills/robsonrung/test-lens/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: robsonrung (https://skillmd.com/u/robsonrung)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/robsonrung/test-lens

---


# Test Lens — Valuable Unit Testing

Judge or write tests through the framework in _Unit Testing: Principles, Practices, and Patterns_ (Vladimir Khorikov). This is **not** the red-green loop (use `tdd` for cadence) and **not** a bug hunt (that belongs in a code-review pass). It answers one question: **is this test worth its maintenance cost, and does it test the right thing the right way?**

The goal of a test suite is **sustainable project growth** — letting you add features and refactor without fear. A small number of highly valuable tests beats a large number of mediocre ones. **Test code is a liability too.** Set a high bar; delete tests that don't clear it.

Examples below are TypeScript + Jest. Apply the lens, then report findings concretely: cite `file:line`, name the rule, propose the fix. If a test is already good, say so plainly.

## The core rubric: the four pillars

Every test is scored 0–1 on four attributes; **value = the four multiplied**. A zero on any pillar makes the test worthless — even if it's perfect on the other three.

1. **Protection against regressions** — how much code (yours + libraries) the test exercises, weighted by complexity and domain significance. Trivial code → low protection.
2. **Resistance to refactoring** — how few **false positives** (failures when behavior is intact) it produces. Driven entirely by coupling to implementation details. **Non-negotiable.**
3. **Fast feedback** — how quickly it runs.
4. **Maintainability** — how hard it is to read and to run (setup, out-of-process deps).

**Key tension:** the first three are mutually exclusive — you can't maximize all three. Since resistance-to-refactoring must stay high, you trade **protection ⇄ fast feedback**. The three classic failure modes, each maxing two pillars and zeroing a third:

| Test type | Sacrifices | Why it's low-value |
| --- | --- | --- |
| End-to-end only | Fast feedback | Great coverage + resilient, but too slow to be the whole suite |
| Trivial (`expect(user.name).toBe('John')` on a plain getter) | Protection | Nothing to catch |
| **Brittle** (asserts SQL string / call order / private structure) | Resistance to refactoring | Breaks on every refactor → ignored → real bugs slip through |

> Brittle tests are the dangerous ones. They train the team to ignore failures, then a real regression rides along with the noise into production.

## Lens 1 — Observable behavior, not implementation details

The **single most important rule.** False positives come from coupling a test to _how_ the code works instead of _what_ it produces. Verify the **end result meaningful to a domain expert / end user** — disregard the steps taken to get there.

Red flags (all couple to implementation):

- Asserting which internal methods were called, in what order, or how many times (on a stub).
- Asserting a generated SQL string, the list/types of internal collaborators, or private state.
- A test that mirrors the production algorithm step-for-step ("leaking domain knowledge").
- A test you must edit every time you rename a private method or reshuffle internals.

> A good test tells a story about the problem domain. If it fails, the story and the code disagree — and that's the only failure worth your attention.

## Lens 2 — Pick the right style (ranked)

| Style | Verifies | Quality | Prefer when |
| --- | --- | --- | --- |
| **Output-based** | return value of a pure function | **Best** — naturally resists refactoring | always, if the code allows |
| State-based | object/system state after the act | OK — can tie to leaking state | mutation is the actual outcome |
| Communication-based | calls to collaborators (mocks) | Most brittle | only at the system edge, for unmanaged deps |

Push code toward **output-based** by separating decisions from actions: a **functional core** (pure business logic, easy to output-test) wrapped in a **mutable shell** (thin, does I/O). This is hexagonal architecture taken to its extreme. See `references/decision-trees.md`.

## Lens 3 — Mocks: stub vs mock, managed vs unmanaged

The rules agents most often get wrong. Full flowchart in `references/decision-trees.md`.

- **Stub** = emulates an _incoming_ interaction (input data the SUT reads). **Never assert calls to a stub** — that's overspecification and the #1 source of fragile tests.
- **Mock** = emulates an _outcoming_ interaction (a side effect the SUT causes). Asserting it is fine _only_ when that side effect is itself the observable outcome.
- **Managed dependency** (you fully control it; only your app touches it — e.g. your DB): interactions are **implementation details** → use the **real thing** in integration tests.
- **Unmanaged dependency** (externally observable — SMTP, message bus, third-party API): interactions are **observable behavior / a contract** → **mock it**.
- Mock **only at the system edge**, and **only types you own** — wrap third-party SDKs in your own adapter and mock the adapter, not the library.

## Lens 4 — What to test at all (the code quadrant)

Classify the code under test on two axes — **complexity/domain significance** × **number of collaborators**:

|  | Few collaborators | Many collaborators |
| --- | --- | --- |
| **High complexity/significance** | **Domain model / algorithms** → unit-test hard (best ROI) | **Overcomplicated** → refactor: split into the two below |
| **Low** | **Trivial** → don't test | **Controllers** → a few integration tests |

> The more important or complex the code, the **fewer collaborators** it should have.

The overcomplicated quadrant (e.g. a fat controller doing real logic _and_ I/O) is the trap. Split it with the **Humble Object** pattern: extract the logic into a pure algorithm/domain class (unit-test it output-based), leaving a thin humble wrapper that just glues to dependencies (cover lightly via integration tests). **100% coverage is not the goal** — it's possible to have high coverage and worthless tests (and vice versa); significant value per test is. Better no test than a bad test.

## Anti-pattern checklist (flag these)

- Testing **private methods** → a private method doing complex work is a missing abstraction; extract a class.
- **Exposing private state** just to assert on it → test observable behavior instead.
- **Leaking domain knowledge** → test hardcodes the algorithm's expected intermediate math.
- **Code pollution** → test-only switches/flags in production code.
- **Mocking concrete classes** to keep half their behavior → SRP violation; split the class.
- **Time as ambient context** (`Date.now()` reached directly) → inject time as an explicit dependency.
- Asserting interactions **with stubs**, or chasing a **coverage number** as the target.

## Mechanics (when writing tests)

- **AAA** — Arrange / Act / Assert, one of each; if you need multiple act sections it's probably an integration test. No `if` statements in a test (split it). Keep the act to one line for a unit of behavior.
- **Name** the test as a domain statement of behavior — `delivery_with_a_past_date_is_invalid`, not `IsValid_PastDate_ReturnsFalse`. No method names in the title; a non-programmer should read it.
- **Parameterize** similar cases (`it.each`) — but keep distinct behaviors as separate tests.
- Reuse fixtures via **factory functions**, not shared mutable state in `beforeEach` (high coupling between tests is an anti-pattern).

## Workflow

1. **Whose test is this?** New code → guide style choice (Lens 2/4) before writing. Existing test under review → score it.
2. **Score the four pillars.** Name the weakest. If any is ≈0, recommend rewrite or deletion.
3. **Apply the lenses** that fit: observable-behavior (always), mock rules (if it has doubles), quadrant (if deciding what/whether to test).
4. **Run the anti-pattern checklist.**
5. **Report** concretely per finding: `file:line` → which rule → the fix. Skip lenses that don't apply rather than padding.

For the detailed decision trees (style transition, mock flowchart, Humble Object refactor, integration-test rules — path selection, logging, pyramid — and the full anti-pattern catalog with before/after code), read `references/decision-trees.md`.

