What It Means to Write a Test First
Writing a test first means specifying the expected behavior of a piece of code before that code exists, then writing the minimum implementation needed to satisfy the specification. Instead of coding a function and checking afterward whether it seems to work, you write an assertion like expect(add(2, 3)).toBe(5) against a function that does not exist yet, watch it fail, and only then write add(). This reverses the usual order: you design the interface and behavior from the caller's perspective before worrying about implementation details.
Cricket analogy: A coach doesn't design a batsman's stroke after the ball is bowled — he sets a target shot, like Virat Kohli drilling a cover drive against a specific line and length, before facing that delivery in a match, just as a test defines the expected outcome before the code exists.
The Red-Green-Refactor Cycle
Test-first development follows a tight loop: write a failing test (red), write the simplest code that makes it pass (green), then improve the code's structure without changing its behavior (refactor). Each cycle should take minutes, not hours. Seeing the test fail first is not a formality — it proves the test can actually detect a wrong or missing implementation, which is the whole point of writing it before the code. A test that passes immediately without an implementation is testing nothing.
Cricket analogy: A bowler practicing yorkers first bowls a full toss to confirm the stump camera and speed gun are actually recording (red), then adjusts run-up and release to land the yorker (green), and finally smooths the action for consistency (refactor).
// Step 1: RED — write the test before the implementation exists
test('add sums two positive integers', () => {
expect(add(2, 3)).toBe(5); // add() is not defined yet — this fails
});
// Step 2: GREEN — write the minimum code to pass
function add(a, b) {
return a + b;
}
// Step 3: REFACTOR — clean up once green, tests stay untouched
const add = (a, b) => a + b;Choosing Your First Test Case
The best first test for a new piece of behavior is usually the simplest possible case that forces you to write real code rather than a hardcoded constant. For a function that sums a list, testing sum([]) equals 0 is weak because it can be satisfied by returning 0 unconditionally; testing sum([2, 3]) equals 5 forces an actual accumulation. Picking a test that is too complex up front, with edge cases and error handling all at once, makes the first red-green cycle slow and defeats the purpose of small, fast increments.
Cricket analogy: A net bowler warming up a new batter starts with a straight half-volley, not a googly with the second new ball, because the first ball needs to force a real shot, not a lucky leave.
A useful rule of thumb: if your first test can pass by returning a hardcoded literal, it hasn't yet earned the right to be called a test of behavior — write a second case that forces a real implementation, a technique known as triangulation.
Common Beginner Mistakes
New practitioners often write the test and the implementation in the same breath, which erases the value of watching red turn to green, or they write a large integration test as their 'first test' when a focused unit test would isolate the behavior far better. Another frequent mistake is testing framework or language behavior instead of your own logic — for example, asserting that JSON.parse works correctly rather than testing how your code handles the parsed result. The test should exercise a decision your code makes, not a guarantee the platform already provides.
Cricket analogy: A rookie umpire who signals a boundary before the ball actually crosses the rope is skipping the verification step, just like writing code and test together skips proving the test can fail.
If you never see your test fail, you cannot trust that it is testing anything at all — a test that is green from the moment it is written may be checking the wrong thing, or nothing.
- Writing a test first means specifying expected behavior before the implementation exists.
- The red-green-refactor cycle proves the test can fail (red) before trusting that it passes (green).
- Choose the simplest first test case that still forces a real implementation, not a hardcoded return value.
- Triangulate with a second, different test case to rule out constant-return implementations.
- Avoid writing test and implementation simultaneously — you lose the verification that the test can catch a bug.
- Test your own decision logic, not guarantees already provided by the language or framework.
- Keep the first cycle small and fast; complex edge cases belong in later iterations.
Practice what you learned
1. Why is it important to see a test fail before making it pass?
2. What is a weakness of using sum([]) equals 0 as the only test for a summing function?
3. What is triangulation in test-first development?
4. Which of these is a common beginner mistake in test-first development?
5. What should a good first unit test primarily exercise?
Was this page helpful?
You May Also Like
TDD for a Simple Feature
Walk through a complete red-green-refactor cycle for a realistic feature, from the first failing test to a clean, general implementation.
Common TDD Pitfalls
Recognize the most frequent ways teams misapply TDD — testing internals, skipping red, and overmocking — and how to avoid each.
Refactoring Safely with Tests
Understand why a solid test suite is what makes structural code changes safe, and how to build one for legacy code that has none.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringDesign Patterns Study Notes
Software Design · 30 topics
Software EngineeringSoftware Engineering Study Notes
Python · 40 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics