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 produce 12.

Running vs Testing
Running vs Testing

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:

InputWhy this case mattersExpected
91above A boundaryA
90exact A boundaryA
89just below AB
80exact B boundaryB
79just below BBELOW B
Boundary Visualization
Boundary Visualization

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.

Expected vs Actual
Expected vs Actual

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.

1 / 5
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.