Unit 14 · lesson
Test Cases, Assertions & Edge Cases
How do you prove a function still works after you change it?
Running the program and looking at the screen is evidence. It is just expensive evidence because a human has to remember what to try and what the correct result should be every time.
Automated tests turn those expectations into repeatable checks.
A test is an executable claim
Start with a function:
def multiply(a, b):
return a * b
A manual check might be:
print(multiply(3, 4))
You see 12, remember that 12 is expected, and decide the function looks correct.
An automated check stores the expectation in code:
actual = multiply(3, 4)
expected = 12
assert actual == expected
Read that assertion as a claim:
For this case,
multiply(3, 4)must produce12.
Diagrams open at a readable shape-aware scale. Zoom or expand when you need more detail.
The useful model is:
known starting conditions
↓
execute behavior
↓
actual result
↓
compare with expected result
↓
pass or fail evidence
One passing case proves one case
This test:
assert multiply(3, 4) == 12
does not prove that every possible input works.
Try to break the claim deliberately:
assert multiply(0, 8) == 0
assert multiply(-3, 4) == -12
assert multiply(-3, -4) == 12
Now the suite covers several different input relationships.
That still does not prove the function for every object Python could accept. Testing is evidence, not mathematical omniscience.
Cases come from the contract
Suppose you write:
def classify_score(score):
if score >= 90:
return "A"
if score >= 80:
return "B"
return "BELOW B"
Do not choose random test values and hope they are useful.
Find the boundaries in the rule:
90
80
Then test around them:
| Input | Why this case matters | Expected |
|---|---|---|
| 91 | above A boundary | A |
| 90 | exact A boundary | A |
| 89 | just below A | B |
| 80 | exact B boundary | B |
| 79 | just below B | BELOW B |
Diagrams open at a readable shape-aware scale. Zoom or expand when you need more detail.
That table came from the behavior contract, not from a desire to make five tests.
A failing test is useful information
Break the code intentionally:
def classify_score(score):
if score > 90:
return "A"
if score >= 80:
return "B"
return "BELOW B"
Which test should fail?
assert classify_score(90) == "A"
The failure is valuable because it identifies a mismatch between required behavior and current implementation.
Do not treat red test output as a punishment. It is a measurement.
Assertions need useful context
A bare assertion can be enough when a testing framework shows the values clearly. During small experiments, a message can help:
actual = classify_score(90)
expected = "A"
assert actual == expected, f"expected {expected!r}, got {actual!r}"
The important part is not decorating every assertion. It is preserving the comparison between actual and expected.
Diagrams open at a readable shape-aware scale. Zoom or expand when you need more detail.
Failure behavior is part of the contract too
Consider:
def calculate_average(scores):
return sum(scores) / len(scores)
What should happen for an empty list?
calculate_average([])
You need a design decision before you can write a meaningful test.
Possible contracts include:
return 0
return None
raise ValueError
reject empty data earlier
The current accidental exception is not automatically the desired behavior.
That is a major testing habit:
Do not freeze implementation accidents into tests before deciding what the software is supposed to do.
Test names should explain the scenario
Compare:
def test_1():
...
with:
def test_score_90_is_classified_as_a():
...
The second name helps another developer understand the behavior being protected.
A test suite becomes a kind of executable documentation when its cases and names express the contract clearly.
Build a tiny case matrix
Choose one function from your project and write a table before writing test code:
NORMAL CASE
BOUNDARY CASE
EMPTY / ZERO CASE
INVALID CASE
PREVIOUS BUG CASE
Not every function needs every category. Decide which ones make sense from its responsibility.
Then convert at least three useful rows into assertions.
What a green test suite does not mean
A green suite can still miss bugs because:
- important cases were never written;
- the test asserts the wrong expected behavior;
- the test never calls the code you think it calls;
- environment/integration behavior is outside the unit test;
- generated tests and generated code misunderstood the same requirement.
That last problem becomes especially important in Unit 15.
For now, remember the stronger statement:
A passing test is evidence that this encoded claim held during this execution.
Lesson 2 adds pytest so those claims can be organized, discovered, run, and diagnosed as a real suite.
Vocabulary lab
Flip the idea, not just the card
Explain the term before you reveal the back. Then compare your explanation with the definition, example, and warning.
Read all terms without animation
- Test Case
- A specific starting condition, action, and expected result used to check behavior. Example: score 90 must classify as A. Do not confuse it with: Running a program without defining the expected result.
- Assertion
- An executable claim that a condition must be true for a test case to pass. Example: assert classify_score(90) == 'A'. Do not confuse it with: Printing a result and judging it by eye.
- Boundary Case
- A test value at or immediately around the edge where behavior changes. Example: 89, 90, and 91 around a >= 90 rule. Do not confuse it with: An arbitrary value far from any decision boundary.
- Expected Result
- The behavior required by the specification or contract for a particular case. Example: An exact score of 90 returns A. Do not confuse it with: Whatever the current implementation happens to return.
- Regression Test
- A test that preserves behavior after a bug has been discovered and repaired. Example: A test ensuring the exact threshold value remains handled correctly. Do not confuse it with: A manual note saying the bug was fixed once.