Why We Test and What to Test
Layer 5 begins. Every session so far has been manually verified by reading printed output. Automated tests replace that manual check with something repeatable and reliable.
A check that runs itself, every time, without being asked.
1. Learning Objective
By the end of this session you will be able to:
- Explain what an automated test verifies that manual print-checking does not
- Distinguish testing behavior (what a function/method does) from testing implementation (how it does it)
- Identify which parts of the Country Explorer project are worth testing first
- Explain what NOT to test, and why over-testing has real costs
- Recognise this as a concept-only session, mirroring Sessions 12, 20, and 27
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
You have manually run print(kenya.summary()) and visually confirmed it looks right, many times across many sessions. What does an automated test add that this process lacks?
A good test for grow_population(amount) checks that self.population increases by amount. Should it also check that self.population is stored as a specific type of internal Python integer object?
Given everything built in Layers 1โ4, which is the best FIRST thing to write tests for?
Why is testing literally everything, including trivial one-line getters with zero logic, often not worth it?
Why does this session, like Sessions 12/20/27 before it, contain very little new syntax?
3. The Concept โ Why We Test
Behavior is the observable result โ what a test should check. Implementation is the internal how โ fragile and usually not worth testing directly.
Manual verification does not scale
Every lab so far ended the same way: run the file, read the printed output, and eyeball whether it looks right. This works for a single session, but it does not scale โ nothing stops a later change from silently breaking Session 14's summary() while you are focused on Session 28's repository.
What an automated test actually gives you
A test is code that runs other code and checks the result automatically, every time, without a human needing to remember to look. If a change anywhere breaks an existing behavior, the test fails loudly and immediately โ instead of the bug silently shipping unnoticed.
# Conceptually โ this is what a test does, without any special tooling yet
def test_summary_format():
k = Country(name="Kenya", region="Africa", population=54000000)
result = k.summary()
assert result == "Kenya (Africa): pop. 54,000,000"
test_summary_format() # runs silently if it passes; raises AssertionError if not
Behavior vs implementation
A good test checks WHAT code does (its observable result), not HOW it does it internally. Testing implementation details makes tests fragile โ they break during harmless refactors that did not actually change any real behavior.
# Good โ tests behavior (the observable result)
def test_grow_population_increases_value():
k = Country(name="Kenya", region="Africa", population=54000000)
k.grow_population(1000000)
assert k.population == 55000000
# Bad โ tests an implementation detail that could change for unrelated reasons
# def test_grow_population_uses_plus_equals_operator():
# ... inspecting the actual bytecode or source of grow_population ...
What to prioritize testing in this project
High value: Country's validated methods (set_population, grow_population), CountryRepository's data-fetching and validation logic, and CountryExplorer's computed properties. Lower value: one-line trivial getters with no logic, and anything that is purely cosmetic printing.
4. Lab
What you will build
A file called manual_tests.py โ using bare assert statements, the same tool pytest builds on.
Step-by-step instructions
Create the file with Country and CountryRepository from earlier sessions
# manual_tests.py
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
def summary(self):
return f"{self.name} ({self.region}): pop. {self.population:,}"
def set_population(self, value):
if value < 0:
raise ValueError(f"population must be non-negative, got {value}")
self.population = value
def grow_population(self, amount):
if amount < 0:
raise ValueError(f"amount must be non-negative, got {amount}")
self.population += amount
Write a plain assert-based check for summary()
def test_summary_format():
k = Country(name="Kenya", region="Africa", population=54000000)
assert k.summary() == "Kenya (Africa): pop. 54,000,000"
test_summary_format()
print("test_summary_format passed")
Write checks for grow_population, including the rejection case
def test_grow_population_increases_value():
k = Country(name="Kenya", region="Africa", population=54000000)
k.grow_population(1000000)
assert k.population == 55000000
def test_grow_population_rejects_negative():
k = Country(name="Kenya", region="Africa", population=54000000)
try:
k.grow_population(-5)
assert False, "expected a ValueError but none was raised"
except ValueError:
pass # expected
test_grow_population_increases_value()
test_grow_population_rejects_negative()
print("grow_population tests passed")
Write a check for set_population preserving previous valid state on rejection
This directly verifies the guarantee established in Session 21.
def test_set_population_rejects_and_preserves_state():
k = Country(name="Kenya", region="Africa", population=54000000)
try:
k.set_population(-1)
assert False, "expected a ValueError but none was raised"
except ValueError:
pass
assert k.population == 54000000, "population should be unchanged after a rejected update"
test_set_population_rejects_and_preserves_state()
print("set_population rejection test passed")
Write a short comment list of things you deliberately did NOT test, and why
E.g. the exact wording of a print() statement, or a trivial getter with no logic.
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
manual_tests.py |
Created | Plain assert-based behavior checks for the highest-value logic in the project. |
docs/sessions/session-31/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add manual_tests.py docs/sessions/session-31/index.html
git commit -m "session-31: write manual assert-based tests for the highest-value logic first"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
You have manually run print(kenya.summary()) and visually confirmed it looks right, many times across many sessions. What does an automated test add that this process lacks?
A good test for grow_population(amount) checks that self.population increases by amount. Should it also check that self.population is stored as a specific type of internal Python integer object?
Given everything built in Layers 1โ4, which is the best FIRST thing to write tests for?
Why is testing literally everything, including trivial one-line getters with zero logic, often not worth it?
Why does this session, like Sessions 12/20/27 before it, contain very little new syntax?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- What would you have to remember to do manually, every single time you changed Country, to catch the same bugs these tests now catch automatically?
- Why does test_grow_population_rejects_negative() check for a ValueError instead of checking that population became a specific wrong value?
- Can you think of a bug from an EARLIER session in this curriculum that one of today's tests would have caught immediately?
- What is the risk of writing too many trivial tests, in terms of your own time and the codebase's long-term maintainability?
10. What Breaks If This Knowledge Is Missing?
- Silent regressions: Without any automated tests, a change made while working on Session 34 could silently break something built in Session 14, and nothing would tell you until a human happened to notice much later โ if ever.
- A proper testing tool (Session 32): The bare assert statements from this session work, but they lack useful failure messages, test discovery, and a clean way to organize many tests. Session 32 introduces pytest to solve exactly these gaps.
- Confident refactoring (Layer 6): Session 36-40's architecture refactoring only becomes safe to do confidently once a real test suite exists to catch anything the refactor accidentally breaks โ this session is the philosophical foundation for that safety net.
11. What We Learned
Python concept mastered: Why automated tests matter, the distinction between testing behavior and testing implementation, and prioritizing what is actually worth testing.
Unlocks: You can now write and reason about basic automated checks, and โ just as importantly โ deliberately choose what NOT to test.
Next session: Session 32 โ Setting Up pytest. We replace bare assert statements with pytest โ a real testing tool with better failure messages, discovery, and organization.