Test-Driven Development (TDD)
What is TDD
Test-Driven Development (TDD) is a development method where you write tests first, then write the implementation.
The traditional development flow looks like this:
- Write the functional code
- (If there's time) Add tests
- Hope the code is correct
TDD reverses this flow:
- First write a test that describes what you expect the code to do
- Write the minimum code to pass the test
- Refactor and optimize under the protection of tests
Note that TDD is not a testing technique — it's a development method. Tests are just the means; the real goal is to help you write better code.
Core Principles of TDD
Red-Green-Refactor
The core TDD cycle has just three steps, repeated continuously:
Red — Write a failing test
You first define "what the code should do." This test must fail because you haven't written the implementation yet. Failure proves the test is working, not a fake test that always passes.
Green — Write the minimum code to pass the test
The goal isn't to write perfect code — it's to make the test pass. Even if the implementation is ugly or hardcoded, as long as the test passes, that's fine. The key word here is "minimum" — don't write more than you need.
Refactor — Optimize the code under the protection of tests
Now that the test passes, you can safely refactor: improve naming, eliminate duplication, optimize structure. If refactoring breaks anything, the tests will tell you immediately.
Then go back to Red, write a test for the next small feature, and repeat.
Test-First Thinking
The most valuable aspect of TDD isn't "having tests" — it's thinking first about "how should the code be used."
When you write tests first, you're designing the API — what the function is called, what parameters it takes, what it returns. This forces you to think through the interface before writing the implementation, rather than writing code first and then backfilling a test.
Good interfaces are often designed from the consumer's (caller's) perspective, and tests are the earliest "consumer."
Small Iterations
TDD requires handling only one small requirement at a time. Don't try to write everything at once — write one test, pass it, then write the next test. Each iteration is small, and the feedback loop is fast.
This lets you always know the state of your code: either all tests pass, or the step you just wrote has a problem. If something goes wrong, it's easy to locate because the changes are small.
The Basic TDD Flow
Let's demonstrate with a simple example. Suppose we want to write a function that checks whether a number is prime.
Step 1: Write the test
# test_prime.py
from prime import is_prime
def test_1_is_not_prime():
assert is_prime(1) is False
Run the test — it fails (Red). Because the is_prime function doesn't exist yet.
Step 2: Write the minimum implementation
# prime.py
def is_prime(n):
return False
Run the test — it passes (Green). That's right, the simplest implementation is to just return False, since the only test case so far is input 1.
Step 3: Add more tests
def test_2_is_prime():
assert is_prime(2) is True
Run the test — it fails (Red). Because now is_prime returns False for all inputs.
Step 4: Update the implementation
def is_prime(n):
if n < 2:
return False
for i in range(2, n):
if n % i == 0:
return False
return True
Run the test — all pass (Green).
Step 5: Refactor
The implementation logic is correct and the code is fairly clean, so no refactoring is needed for now.
Continue adding tests:
def test_4_is_not_prime():
assert is_prime(4) is False
def test_7_is_prime():
assert is_prime(7) is True
def test_9_is_not_prime():
assert is_prime(9) is False
All pass. With each test you add, your confidence in the code grows.
Why TDD Matters for AI-Assisted Programming
Challenges of AI-Generated Code
AI programming tools (like Claude, GitHub Copilot) can generate code quickly, but they also bring new problems:
- "Looks right" doesn't mean it's right. AI-generated code is usually syntactically correct and logically smooth, but may fail under edge cases.
- You may not fully understand the generated code. If you can't read the code, how can you be sure it's correct?
- Without verification, confidence is low. Without tests, you can only manually try a few examples and hope nothing breaks in production.
The essence of these problems is: AI has dramatically reduced the cost of writing code, but hasn't reduced the cost of verifying code correctness.
How TDD Solves These Problems
Tests as specifications. Before asking AI to generate code, you write the tests first — tests are your precise definition of requirements. With tests as context, AI is more likely to generate code that meets your expectations.
Automated verification. AI-generated code can be tested immediately. If it passes, the implementation meets the specification; if not, it needs adjustment. No need to review every line of AI code manually.
Safety net. When you need to modify or refactor AI-generated code, tests tell you if you've broken anything. This is especially important when doing secondary development on top of AI code.
Gradual trust. Through a set of tests, you can gradually build confidence in AI code. The more comprehensive the test coverage, the greater the confidence.
TDD Practice in AI-Assisted Programming
Workflow
In AI-assisted programming, the typical TDD workflow is:
- You describe the requirements in natural language
- AI generates the test code
- You review the tests — ensure they cover the correct scenarios
- AI generates the implementation code
- Tests automatically verify whether the implementation is correct
- You decide whether refactoring is needed
Key point: tests are the "contract" between humans and AI. You tell AI what you want through tests, and AI satisfies the tests with its implementation.
Writing Good Prompts for AI
Including tests as part of your prompt produces much better results:
Please implement a Python function is_prime(n) that passes the following tests:
def test_1_is_not_prime():
assert is_prime(1) is False
def test_2_is_prime():
assert is_prime(2) is True
def test_4_is_not_prime():
assert is_prime(4) is False
Compared to just saying "write a function to check if a number is prime," providing tests helps AI understand your requirements more accurately.
Caveats
Test quality still needs human oversight. AI may generate tests that "pass but have logic errors" — for example, the test itself has a bug, or the test coverage isn't comprehensive enough. You can't hand off tests entirely to AI and forget about them.
Edge cases require human initiative. AI won't automatically think about "what if the input is negative?" or "what if the input is 0?" These edge cases need you to proactively add tests.
Don't do TDD for TDD's sake. If it's a simple script or one-off data processing, strict TDD isn't necessary. TDD is best suited for code that needs long-term maintenance and has clear inputs and outputs.
Summary
TDD is more valuable in the AI era than ever before. AI has made writing code cheap, but verifying code correctness remains the developer's core responsibility. TDD provides a systematic approach: using deterministic tests to tame the uncertainty of AI-generated code.
For university students, learning TDD isn't just about learning a development method — it's about cultivating the programming habit of "thinking clearly before acting." This habit is especially important in the age of AI-assisted programming — because AI can write code for you, but only you can decide what the code should do.