TDD Isn’t Slowing You Down. It’s Helping You Ship the Right Thing Faster
by Gary Worthington, More Than Monkeys

We all know we should write tests. Just like we should stretch before we run, or put the toilet lid down before we flush. But when deadlines are tight and Jira is on fire, it’s easy to treat testing as a luxury, or worse, something the QA team will “catch later”.
Tests aren’t just about checking that code works. When done with intent via Test-Driven Development (TDD), they shape the code you write. They help you move faster, not slower. And they give you confidence that “done” really means done.
In this post, I’ll walk through how TDD works in practice using Python. We’ll cover:
- What TDD is (and what it isn’t)
- Writing your first test before the function
- Letting tests drive the design
- How refactoring becomes safe and fast with tests
- Why skipping tests slows you down later
What is TDD?
TDD is a development workflow that flips the usual order on its head:
- Write a failing test
- Write the simplest code to make it pass
- Refactor with confidence
It’s not about writing tests for every tiny thing. It’s about using tests as a tool to design the right behaviour and catching bugs before they exist.
Let’s Build Something Using TDD: A Discount Function
Say you’ve been asked to implement a discount system for members. Here’s how TDD guides you through it.
Step 1: Write the first test
def test_discount_applied_for_members():
assert calculate_discount(100, True) == 90
This will fail because calculate_discount doesn’t exist yet. Exactly what we want….
Step 2: Make it pass with the simplest code
def calculate_discount(price, is_member):
return price * 0.9 if is_member else price
Run the test and as expected, it now passes.
Add more tests
Next, we cover the non-member case.
def test_no_discount_for_non_members():
assert calculate_discount(100, False) == 100
You already wrote logic to support this, so it passes too.
At this point, you’ve covered both paths through the function. The code is simple, and tests are locking in behaviour.
Now Let the Tests Drive Evolving Requirements
Product now tells you we need:
- 10% discount for standard members
- 20% for premium members
- No discount otherwise
First, add (failing) tests to reflect this behaviour:
def test_discount_for_standard_members():
assert calculate_discount(100, "standard") == 90
def test_discount_for_premium_members():
assert calculate_discount(100, "premium") == 80
def test_no_discount_for_others():
assert calculate_discount(100, None) == 100
All three tests fail, as expected (as we’ve updated the function signature in the tests). Let’s update the function to make them pass.
def calculate_discount(price, membership_level):
if membership_level == "premium":
return price * 0.8
elif membership_level == "standard":
return price * 0.9
return price
Run the tests and all we see is green. You’ve now implemented multi-tiered pricing behaviour with the safety net of tests backing every decision.
Time for a Refactor
Your function is growing. Let’s refactor using TDD’s third step: make it better, but keep the tests green.
Extract the discount logic into a separate class:
class DiscountCalculator:
def __init__(self, membership_level):
self.membership_level = membership_level
def get_discount_rate(self):
return {
"premium": 0.2,
"standard": 0.1
}.get(self.membership_level, 0)
def apply_discount(self, price):
return price * (1 - self.get_discount_rate())
Refactor calculate_discount:
def calculate_discount(price, membership_level):
return DiscountCalculator(membership_level).apply_discount(price)
You don’t need to rewrite tests. You already have them. And they all still pass. That’s the payoff of TDD: freedom to improve your code without fear.
A Real-World TDD Refactor: Parsing Invoice Lines
Let’s move to a slightly messier example, this time one based on parsing invoice lines from CSV.
Step 1: Write a test first
def test_parses_invoice_line_correctly():
line = "Widget,2,19.99"
result = parse_invoice_line(line)
assert result == {
"description": "Widget",
"quantity": 2,
"unit_price": 19.99,
"total": 39.98
}
Step 2: Make it pass
def parse_invoice_line(line):
parts = line.split(",")
description = parts[0].strip()
quantity = int(parts[1])
unit_price = float(parts[2])
total = quantity * unit_price
return {
"description": description,
"quantity": quantity,
"unit_price": unit_price,
"total": total
}
It works. But it’s fragile. Add a test to handle malformed input:
import pytest
def test_invalid_line_raises_error():
with pytest.raises(ValueError):
parse_invoice_line("just one field")
It fails, as expected. So now improve the implementation to pass:
def parse_invoice_line(line):
parts = line.split(",")
if len(parts) != 3:
raise ValueError("Invalid invoice line format")
description = parts[0].strip()
quantity = int(parts[1])
unit_price = float(parts[2])
total = quantity * unit_price
return {
"description": description,
"quantity": quantity,
"unit_price": unit_price,
"total": total
}
Again, TDD means we evolve the behaviour through tests. The function becomes safer, more robust, and well-documented, all without slowing us down.
TDD Isn’t About Perfection. It’s About Progress with Confidence
Some developers skip TDD because they think it’ll slow them down. Ironically, the opposite is true.
When you don’t write tests:
- You move fast at first
- Then spend days debugging issues later
- Or waste time manually re-checking things after each change
- Or worse, ship silent regressions into production
That “quick delivery” starts to look pretty expensive.
With TDD:
- You catch mistakes early
- You only write the code you need
- You can change your mind safely
- You gain real confidence in your changes
- You can be brutal in your refactors, as your tests have locked in your required behaviour
What Makes a Good TDD Test?
Some good rules of thumb:
✅ Write the test before the implementation
✅ Keep each test focused on one behaviour
✅ Don’t overdo mocks unless there’s an external dependency
✅ Let the tests guide what you build next
If a function is hard to test, it’s usually trying to do too much. TDD gives you an early warning and nudges you toward better design.
Final Thought: Red → Green → Refactor

TDD isn’t a testing technique. It’s a thinking tool.
It forces you to slow down just enough to define the behaviour you actually want before you write the code to make it happen. That tiny pause to write the test unlocks enormous speed later.
You write fewer bugs. You spend less time in hotfix mode or in manual run->test->fix cycles. And your team gains confidence in what they’re building.
So next time you’re tempted to skip the test “just this once”, try flipping it. Write the test first. Let it fail. Then go green.
And then refactor like a boss.
I’ve seen TDD transform how teams deliver, not just by catching bugs early, but by shaping better code, faster feedback, and cleaner architecture. If your team treats testing as an overhead, it’s time to flip the narrative.
Start with just one test. Let it drive the next step. And watch how it changes the way you think about shipping software.
Gary Worthington is a software engineer, delivery consultant, and agile coach who helps teams move fast, learn faster, and scale when it matters. He writes about modern engineering, product thinking, and helping teams ship things that matter.
Through his consultancy, More Than Monkeys, Gary helps startups and scaleups improve how they build software — from tech strategy and agile delivery to product validation and team development.
Visit morethanmonkeys.co.uk to learn how we can help you build better, faster.
Follow Gary on LinkedIn for practical insights into engineering leadership, agile delivery, and team performance