Approach · shipping · in production
Tests as Design
I use tests as the initial design specification for new features, ensuring the API is usable and well-defined before writing implementation code.
Tests as Design is the practice of writing a failing test suite before any implementation code, using the test itself to define the desired interface and behavior. This approach forces a clear contract between the consumer and the producer of a function or component.
What it is
This is not merely Test-Driven Development (TDD); the emphasis shifts from testing to designing through testing. When I start a new feature, I don't open the implementation file. I open the `test.ts` file. There, I write the `describe` and `it` blocks that articulate how I expect to interact with the new functionality. This includes defining the function signatures, required input parameters, and expected outputs or side effects. The test becomes the first draft of the API, demanding a specific shape from the code that will eventually make it pass. It's a concrete specification, not an abstract idea.
Why I do it this way
I learned the hard way that unclear interfaces lead to brittle systems and expensive refactoring. By writing the test first, I am forced to think about the consumer's perspective immediately. This prevents over-engineering and ensures the API is intuitive and robust, rather than just functional. It also significantly streamlines my Agent-First Development workflow. When I hand a failing test suite to Claude Code, the agent has an unambiguous target. It's not guessing what I want; it's making the test pass within the defined interface. This clarity reduces iteration cycles and improves the quality of the generated code. For instance, when adding a new payment flow for Credit Packs over Subscriptions on Inky, defining the expected `handleSuccessfulPayment` function and its assertions in a test first ensures the Stripe webhook integration is clean and predictable.
How it works in practice
When a new feature is identified—say, a new backend service for Total Formula 1 or a content generation step for Pregnancy Power Hour—the first step is to create a new test file or add to an existing one. I write the `it` blocks, describing specific scenarios and their expected outcomes. For a Firebase callable function, I'd define its expected input and output types directly in the test call. For a UI component, I'd define the props it expects and assert its rendered output. This process naturally leads to type errors and runtime errors initially, which is the point. These errors highlight the missing implementation and guide the creation of the actual code. Only once the test clearly expresses the desired behavior and interface do I begin writing the minimal implementation code required to make that test pass. This applies across my stack, from Vercel edge functions to Resend email triggers and Stripe payment handlers that update user balances managed by Mercury.
Where this breaks down
This approach is less effective for highly experimental, throwaway code where the primary goal is rapid exploration without a long-term commitment to design. It can also feel slower initially, as the upfront design work is more explicit. For trivial getters and setters, the overhead of writing a dedicated test might not always justify the design benefit. Additionally, if requirements are extremely vague or constantly shifting, the test itself becomes a moving target, undermining the stability that this practice aims to provide. However, for most durable features that contribute to Multiple Revenue Streams, One Back Office, the investment pays off quickly in reduced bugs and clearer architecture.
FAQs
- Isn't this just Test-Driven Development (TDD)?
- Yes, it builds on TDD principles. The distinction is the explicit emphasis on using the test as the primary design tool for the API or interface, rather than just a verification step after design.
- Does this slow down initial development?
- It shifts time upfront to design and interface definition. While initial coding might seem slower, it significantly reduces rework, debugging, and refactoring later, leading to faster overall delivery of stable features.
I run this and four other brands. Want to see the operator playbook in detail?
Get the Builder's Playbook →