Testing Fundamentals
Advanced · 16 min read · ▶ live playground · ✦ checkpoint
A bug does not need permission to come back. You fix a coupon bug today, then a tiny change next week quietly breaks it again. A test is a small check that compares what your code actually does with what you expect, so you do not have to trust memory, hope, or a long manual ritual.
Why tests make you faster
Testing can feel like extra work when the program is tiny. That feeling fades the first time you change one line and wonder, "Did I break anything else?"
Think of tests as a pre-flight checklist. The checklist does not fly the plane for you. It catches the boring, repeatable mistakes before they become expensive. In code, that means you can change a function, run the checks, and get evidence in seconds.
A regression is a bug that returns after it was already fixed. Tests are especially good at catching regressions because they preserve the exact example that used to fail.
Here is a small price rule. If the order reaches 100 dollars, the customer gets free shipping:
def shipping_label(order_total):
if order_total >= 100:
return "free shipping"
return "standard shipping"
print(shipping_label(120))
print(shipping_label(75))free shippingstandard shipping
That output looks fine. But one example is not enough. What about exactly 100? A boundary case is input exactly at an edge where behavior changes, and bugs like to hide there.
Manual testing
Manual testing means you run the program yourself and inspect the result with your eyes. It is useful when you are exploring a new idea, checking a screen, or making sure the whole experience feels right.
Manual checks are also fragile. You forget one case. You skim an output. You run the easy case and miss the edge.
You can make a manual check clearer by printing both the expected result and the actual result:
def shipping_label(order_total):
if order_total >= 100:
return "free shipping"
return "standard shipping"
examples = [
(120, "free shipping"),
(100, "free shipping"),
(75, "standard shipping"),
]
for total, expected in examples:
actual = shipping_label(total)
print(f"total={total} expected={expected} actual={actual}")total=120 expected=free shipping actual=free shippingtotal=100 expected=free shipping actual=free shippingtotal=75 expected=standard shipping actual=standard shipping
This is already better than clicking around randomly. It names the cases. It keeps the boundary case. But you still have to read every line and notice a mismatch.
Automated testing
Automated testing means code checks your code and reports whether the expected behavior still holds. Instead of reading every line yourself, you write claims that Python can verify.
The simplest tool is assert, which you met as defensive programming in Errors and Exceptions. An assertion is a claim that must be true at that point in the program; if it is false, Python raises AssertionError.
def shipping_label(order_total):
if order_total >= 100:
return "free shipping"
return "standard shipping"
assert shipping_label(120) == "free shipping"
assert shipping_label(100) == "free shipping"
assert shipping_label(75) == "standard shipping"
print("shipping checks passed")shipping checks passed
No news is good news with assert: if every claim is true, the program continues. If a future edit changes >= to >, the second assertion catches the exact case you protected.
That is the speed gain. You are not writing tests because you love ceremony. You are turning old examples into a reusable safety check.
Try it - turn examples into checks
Run this code once. Then change the rule for a gold badge from >= 90 to > 90 and run it again. The printed manual lines help you see the story; the assertions stop the program the moment the rule breaks.
Three shapes of tests
Not every test looks at the same amount of the program. Use these names to talk clearly about the size of the check.
A unit test checks one small piece of code, usually one function or method, away from the rest of the system. Unit tests are like checking one recipe card before you plan the whole dinner.
def coupon_discount(subtotal, coupon_code):
if coupon_code == "SAVE10":
return subtotal * 0.10
return 0
assert coupon_discount(80, "SAVE10") == 8
assert coupon_discount(80, "NOPE") == 0
print("unit checks passed")unit checks passed
An integration test checks whether several pieces work together. The point is not one recipe card anymore; it is whether the kitchen stations pass the plate correctly.
def coupon_discount(subtotal, coupon_code):
if coupon_code == "SAVE10":
return subtotal * 0.10
return 0
def order_total(prices, coupon_code):
subtotal = sum(prices)
discount = coupon_discount(subtotal, coupon_code)
return subtotal - discount
assert order_total([20, 30, 50], "SAVE10") == 90
assert order_total([20, 30, 50], "NOPE") == 100
print("integration checks passed")integration checks passed
An end-to-end test checks a whole user-shaped flow from start to finish. In a real app, that might mean opening a page, filling a form, and confirming that the saved result appears. In this browser Python lesson, we can still model the idea with a small function that takes user-like input and returns a final message.
def coupon_discount(subtotal, coupon_code):
if coupon_code == "SAVE10":
return subtotal * 0.10
return 0
def order_total(prices, coupon_code):
subtotal = sum(prices)
discount = coupon_discount(subtotal, coupon_code)
return subtotal - discount
def checkout_message(player, prices, coupon_code):
total = order_total(prices, coupon_code)
return f"{player}, your total is ${total:.2f}"
assert checkout_message("Mina", [20, 30, 50], "SAVE10") == "Mina, your total is $90.00"
print("end-to-end check passed")end-to-end check passed
The categories overlap in real projects, and teams argue about names. Keep the practical difference: small piece, pieces together, full flow.
What to test first
Start with behavior that would embarrass you if it broke:
- Boundaries, like exactly
100dollars or exactly90points. - Past bugs, because regressions are common.
- Money, permissions, saved data, and anything users depend on.
- Small pure functions, because they are easy to check without setup.
Do not test every line. Test promises. If a function promises "a score of 90 earns gold," write that check. If the implementation changes later but the promise still holds, the test should still pass.
Checkpoint
Answer all three to mark this lesson complete
You now have the reason for testing and the basic map of test sizes. Next, you turn these raw assert checks into real Python test cases with the standard library tool teams can run as a suite.