Writing Failing Unit Tests First: Defining Requirements as Tests
Stop coding blindly. Learn to define your requirements as failing unit tests first, ensuring every line of code serves a specific, verified purpose.

Previously in this course, we explored The Red-Green-Refactor Cycle: Master the TDD Workflow, which introduced the rhythmic discipline of Test-Driven Development (TDD). In this lesson, we focus specifically on the "Red" phase: the art and science of writing a failing unit test to define a requirement before a single line of production code exists.
Writing tests first isn't just about catching bugs; it’s about requirements engineering. By writing a test, you are forced to define the contract of your code—what it takes as input, what it returns, and how it handles edge cases—before you get distracted by implementation details.
Defining Requirements as Tests
When we write tests first, we treat the test suite as the living documentation of our requirements. If you cannot describe a requirement as an assertion in a test, you likely do not understand the requirement well enough to code it.
Before we write code, we must ask:
- What is the expected behavior? (The Requirement)
- What is the simplest input that triggers this? (The Setup)
- What is the exact output we expect? (The Assertion)
If you skip this and jump straight to implementation, you risk "gold-plating" your code—adding features or complexity that were never asked for and aren't being tested.
Worked Example: The Currency Converter
Let's say our project requires a CurrencyConverter class that converts USD to EUR. Our requirement is simple: 1 USD equals 0.92 EUR.
Instead of writing the class, we start by writing a test. Using a standard framework like Jest or PyTest, we define the requirement first:
PYTHON# test_converter.py from converter import CurrencyConverter def test_convert_usd_to_eur(): converter = CurrencyConverter() result = converter.convert(1.0, "USD", "EUR") assert result == 0.92
Crucial Step: When you run this, it must fail. If it passes, you have a ghost test or an existing implementation you didn't account for. The error should be clear: ImportError: cannot import name 'CurrencyConverter'.
This failure is perfect. It tells us exactly what we need to build next: a class named CurrencyConverter with a method convert.
Confirming the "Right" Failure
A common mistake is writing a test that fails for the wrong reason. Suppose you write a test for a feature, and it fails with a generic syntax error or an environment issue. That doesn't help you design the feature.
You want a "business logic" failure. In our example, once we create an empty CurrencyConverter class, the test should fail with an AttributeError (because the convert method is missing). Once we add the method, it should fail with an AssertionError (because the return value is None instead of 0.92).
Never proceed until the test fails with the specific error you expect. This confirms that your test is actually exercising the path you intend to build.
Hands-on Exercise
For our running project, let's implement a calculate_tax function.
- Create a new test file.
- Define a test that expects a
10%tax rate on a100.00input to return10.00. - Run the test and observe it failing.
- Verify the failure message is related to the missing function or an incorrect return value, not a setup error.
Common Pitfalls
- Testing Implementation, Not Behavior: Don't write tests that check how the code calculates the tax (e.g., "did it use a loop?"). Test the outcome (e.g., "is the total correct?"). This allows you to refactor the internal logic later without breaking the test.
- The "Big Bang" Test: Don't try to write one giant test that covers every requirement. Break requirements down into the smallest possible unit—one test, one assertion.
- Ignoring the Red Phase: If you write the code first, you are simply "verifying" your work. That’s First Steps into Unit Testing: Automate Your Quality Assurance, but it isn't TDD. You lose the design feedback loop that "Red" provides.
FAQ
Q: What if I can't write a test for a requirement? A: That is a massive red flag. It means the requirement is ambiguous or too complex. Break it down until you can define a single, verifiable outcome.
Q: Is it a waste of time to write tests for code that doesn't exist? A: It feels like it at first, but it saves hours of debugging later. You aren't "writing tests"; you're "designing the interface."
Q: Should my tests fail with an error or an assertion failure?
A: Both are fine. An ImportError or AttributeError confirms the structure is missing. An AssertionError confirms the logic is missing. As long as you know why it failed, you are in the right state.
Recap
We’ve learned that the "Red" phase of TDD is about defining clear, atomic requirements. By forcing a failure, we prove that our test suite is sensitive to changes and that our development is driven by actual, verified needs. We avoid over-engineering by building exactly what the test demands.
Up next: Implementing Minimal Code — we will finally satisfy these failing tests with the smallest amount of code possible.
Work with me

Next.js E-commerce Store Development
Turn your Facebook page or small shop into a real online store — fast, mobile-first, and built to sell. Own your storefront, not just a social page.

Next.js Website & Landing Page Development
A blazing-fast, SEO-optimized website or landing page in Next.js — the kind that loads instantly and ranks. Design-to-code, done right.


