A test suite is either fragile or durable, and the difference is not in size. A fragile suite goes red from a refactoring that did not change behavior: you rename a method, reorder fields - half the tests fall, though a user would notice nothing. A durable suite behaves the opposite way: it stays quiet while behavior is unchanged and fails only when behavior is genuinely broken. Such a suite is built not by luck but by a handful of patterns worth knowing by name.
Durability starts with a single test. Three things keep it in shape. First - no shared mutable state between tests: each one gets its own fresh world, otherwise run order starts to affect the result and a floating lie sets in. Second - the arrange-act-assert structure: data first, then the action, then the check. Third - no logic in the test itself: no if, no loop, no computing the expected value the same way the code does - a test must assert a concrete answer, not restate the implementation. And names: 'returns a zero discount for an empty cart' speaks of behavior, 'test1' of nothing.
When there are many scenarios, a Test Data Builder helps - a factory that assembles a valid object with sensible defaults. The test does not need an order with twenty fields; it cares about two - let the factory fill in the rest, and only what matters to the scenario stays in view. Same-shaped cases with different inputs and outputs are conveniently folded into a table via test.each: one check, rows of data - and the discount threshold boundaries are listed outright, without copies of identical tests.
For the domain to be testable quickly at all, it is decoupled from the outside world - this is Ports & Adapters. The domain depends not on a concrete database or the system clock but on interfaces: Clock gives the time, Repository stores, Gateway reaches outward. These are the ports. In the test a fake is put in their place - a simple in-memory implementation - directly, through an argument or the constructor, without module magic like jest.mock. Less magic means less that breaks when moving to ESM.
Some code is hard to test by its very nature - a controller, a request handler, the React glue between an event and state. The Humble Object pattern says: keep that layer thin and dumb. Let the framework glue only take input and call an ordinary function, while all the decisions - discount calculation, validation, branching - live in a plain module that is checked as a pure unit, without bringing up the framework. The heavy boundary is left nearly empty.
As soon as a port has more than one implementation - an in-memory fake and a real adapter to Postgres - there is a risk they will drift apart: the fake behaves unlike the database, and the green tests lie. Contract Tests close this: the very same set of checks is run against both implementations. The shared contract is described once as a function, which is then fed both factories. As long as both pass the same tests, the fake remains an honest stand-in for the production adapter.
These patterns share one price - design discipline: the interfaces, the factories, and the thin glue must be set up in advance, which on an empty project feels superfluous. They earn their place where code lives long and changes often - in a domain with rules, not in a one-off script. And what a failure without them looks like, everyone knows: an order is mutated right inside a shared object, a neighboring test catches someone else's fields, someone duplicates the discount formula from the code inside the test - and both go green on one and the same bug. A durable suite catches a regression; a fragile one catches renames.
export function repositoryContract(
name: string,
createRepo: () => UserRepository,
) {
describe(name, () => {
test('saves and reads a user', async () => {
const repo = createRepo()
await repo.save(user)
await expect(repo.find(user.id)).resolves.toEqual(user)
})
})
}
repositoryContract('memory', () => new InMemoryUserRepository())
repositoryContract('postgres', () => new PostgresUserRepository(pool))