# What a good test is A test verifies behaviour through a public interface. The code underneath can change entirely; the test should not. A good one reads like a specification: `user can checkout with valid cart` tells you exactly what capability exists, and survives refactors because it does not care about internal structure. ## Good ```typescript test("user can checkout with valid cart", async () => { const cart = createCart(); cart.add(product); const result = await checkout(cart, paymentMethod); expect(result.status).toBe("confirmed"); }); ``` - Tests what a caller cares about - Uses the public interface only - Survives internal refactors - Describes *what*, not *how* - One logical assertion ## The three anti-patterns ### Implementation-coupled Mocks internal collaborators, reaches for private methods, or asserts on call counts and ordering. **The tell: the test breaks when you refactor, even though the behaviour did not change.** ```typescript // BAD: asserts on the mock, not the behaviour test("checkout calls paymentService.process", async () => { const mockPayment = vi.mock(paymentService); await checkout(cart, payment); expect(mockPayment.process).toHaveBeenCalledWith(cart.total); }); ``` A second form is verifying through a side channel instead of the interface: ```typescript // BAD: bypasses the interface to check the database directly test("createUser saves to database", async () => { await createUser({ name: "Alice" }); const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]); expect(row).toBeDefined(); }); // GOOD: verifies through the interface a caller would use test("createUser makes the user retrievable", async () => { const user = await createUser({ name: "Alice" }); const retrieved = await getUser(user.id); expect(retrieved.name).toBe("Alice"); }); ``` ### Tautological The expected value is recomputed the way the code computes it, so the test passes by construction and can never disagree with the implementation. ```typescript // BAD: the assertion reimplements the function test("calculateTotal sums line items", () => { const items = [{ price: 10 }, { price: 5 }]; const expected = items.reduce((sum, i) => sum + i.price, 0); expect(calculateTotal(items)).toBe(expected); }); // GOOD: an independent, known literal test("calculateTotal sums line items", () => { expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15); }); ``` Expected values come from an independent source of truth: a known-good literal, a worked example, or the spec. Never from the code. ### Horizontal slicing Writing all the tests first, then all the implementation. Covered in `SKILL.md`; the fix is vertical slices, one test and one implementation at a time. ## Before writing any test Name the production change that would make this test fail. If you cannot name one, the test asserts nothing and should not be written. ## Keep test-only code out of production A method that exists so a test can reach inside is a hole in the interface. Put helpers in test utilities. If the test cannot reach what it needs through the interface, that is a design finding, not a reason to widen the interface.