Chapter 18: Proving Your Code Works (Testing & TDD)
Learning Objectives
- Understand why manual testing does not scale.
- Learn the concept of Unit Tests and Assertions.
- Understand Test-Driven Development (TDD).
- Learn to write testable, modular code.
Prerequisites
Chapter 5: Functions.
Why Does This Exist?
You write a checkout system. You manually click the "Buy" button to ensure it works. It does.
Six months later, a coworker modifies the tax calculation formula. They deploy the code. Suddenly, the "Buy" button charges everyone $0.
In a 1-million-line codebase, modifying one file can secretly break a completely unrelated file. You cannot manually click every button in your app every time you save a file. That would take days.
We needed a way to write robot-scripts that automatically run through thousands of scenarios in milliseconds, proving the code still works. We needed Automated Testing.
History
When NASA wrote code for the Apollo missions, they couldn't just "deploy and see if it crashes". Lives were on the line. They pioneered rigorous software verification.
In the late 1990s, Kent Beck revolutionized the industry by introducing Test-Driven Development (TDD). The radical idea was: Write the test before you write the actual code.
Mental Model
Think of Automated Tests as a giant net under a trapeze artist.
Without a net, every jump (code change) is terrifying. You move slowly, paralyzed by fear of breaking production.
With a net, if you make a mistake, you just bounce off the net instantly (a red error in your terminal). You can code incredibly fast, refactoring massive systems fearlessly, because the Tests will instantly scream if you break a rule.
Internal Working
A "Test" is nothing more than a normal function that uses the keyword assert.
An assertion tells the CPU: "I believe statement X is True. Evaluate it. If it is False, immediately throw an Exception and crash this test."
A Test Runner (like pytest in Python or Jest in JS) scans your entire codebase, finds all functions starting with the word test_, runs them all, and reports how many assertions survived.
Syntax
Using standard Python (and pytest):
1# The actual code (math_utils.py)
2def add(a, b):
3 return a + b
4
5# The test code (test_math.py)
6def test_add_positive_numbers():
7 result = add(2, 3)
8 assert result == 5
9
10def test_add_negative_numbers():
11 assert add(-1, -1) == -2
Token breakdown:
test_: A strict naming convention. The runner ignores functions that don't start with this.assert: The core instruction. Ifresult == 5evaluates to False, the test fails, and prints a giant red error message.
Visual Explanation
Tiny Example
Testing an edge case.
1def test_empty_cart_total():
2 cart = ShoppingCart()
3 # We expect the total to be 0.00, not crash with a NullPointer error!
4 assert cart.get_total() == 0.0
Common Mistakes
Testing Implementation Details
Why it fails: Imagine you write a test that checks assert cart._internal_array.length == 3. Later, you realize a Dictionary is faster than an Array, so you rewrite the Cart class. Your Cart works perfectly, but the test fails because _internal_array no longer exists. You have created "Brittle Tests".
The Fix: Only test the Public Behavior (the outputs), not the Private Implementation (how it achieved the output). Test assert cart.get_item_count() == 3.
Debugging
When a test fails, do not immediately rewrite the test to make it pass!
If test_tax_calculation turns red, you must trust the test. The test is a historical monument left by a past engineer stating: "Tax MUST work this way." If your new code broke it, you have discovered a bug before it reached production. Fix your new code.
Mini Project
Time: 20 minutes.
Goal: TDD Palindrome Checker.
Do NOT write the logic yet. First, write 3 tests for a function called is_palindrome(word). Test "racecar" (True), "hello" (False), and "A man a plan a canal Panama" (True). Run the tests; watch them fail (because the function doesn't exist). NOW, write the function logic until all three turn Green.
Bigger Project
Time: 1 hour.
Goal: Mocking external dependencies.
You have a function get_weather() that makes a real HTTP request to an API. If you run a test on this, and the API is offline, your test fails even though your code is perfect. Read the documentation for your language's "Mocking" library (e.g., unittest.mock in Python). Write a test that intercepts the network call and instantly returns fake JSON data, ensuring your test is completely offline and lightning-fast.
Production Usage
In top tier tech companies, you cannot merge code into the main Git branch if your tests fail.
A cloud computer automatically downloads your code and runs 10,000 tests. If even one test turns red, the "Merge" button is physically disabled. This is called Continuous Integration (CI).
Companies track "Code Coverage"—the percentage of their entire codebase that is executed during tests. A coverage of 80% is the industry standard for a healthy project.
Best Practices
- Unit vs Integration Tests: A Unit Test isolates a single function in a void. An Integration Test tests how the Cart function talks to the real Database function. Write 80% Unit Tests (fast) and 20% Integration Tests (slow).
- AAA Pattern: Structure every test in three blocks: Arrange (setup the variables), Act (call the function), Assert (check the result).
Interview Questions
Easy: What does an assert statement do?
Answer: It verifies that a condition is true. If it is false, it throws an error and fails the test.
Medium: What is Test-Driven Development (TDD)?
Answer: A methodology where you write a failing test first, write the minimum amount of code to make it pass, and then refactor the code for cleanliness. Red, Green, Refactor.
Hard: Why do we Mock external services in Unit Tests?
Answer: Unit tests must be deterministic and lightning fast. If we rely on real databases or APIs, a network timeout could cause the test to fail, giving a false negative. Mocking replaces the external service with a controlled dummy object.
Revision Sheet
- Automated Testing: Scripts that prove your code works.
- Unit Test: Testing a single, isolated function.
- Mocking: Faking external APIs or databases to keep tests fast and offline.
- TDD: Writing the test before the logic.
Connections
- Previous Chapter: You must heavily mock the Network Requests (Chapter 16) when writing tests.
- Future Chapters: As code gets huge, it becomes "untestable" if everything is tangled together. We need Architecture (Chapter 19) to split code into small, testable chunks.