What proved the refactor was safe
In lesson 1-2 you refactored count_vowels from a loop into a one-line generator expression. The proof that the refactor was safe was that the three untouched asserts still passed afterwards.
That is the safety net idea in one sentence. Tests pin down behavior, so any change that keeps them green is proven to preserve that behavior. Shorter code and careful reading help you write the change, but only the unchanged, passing tests are evidence.
The word untouched carries the weight here. Tests edited during a refactor prove nothing at all, because you can always make a test agree with whatever the code now does.
The three-part shape of every test
Almost every good test has the same skeleton, called arrange, act, assert (AAA):
- Arrange: set up the input data and any objects you need
- Act: call the one function or method under test, once
- Assert: check the result against what you expected
Why enforce a shape? Because tests are documentation. A teammate (or you, in six months) should be able to open any test and answer three questions instantly: what was the situation, what did we do, what should have happened. When those three phases blur together, a failing test becomes a puzzle instead of a message.
One test in AAA shape
A test of cart_total with the three phases marked by comments and exactly one act line.
def cart_total(prices, tax_rate): subtotal = sum(prices) return round(subtotal * (1 + tax_rate), 2) def test_cart_total_applies_tax(): # Arrange prices = [10.0, 5.0] tax_rate = 0.10 # Act total = cart_total(prices, tax_rate) # Assert assert total == 16.5 test_cart_total_applies_tax() print("PASS test_cart_total_applies_tax")
Output
PASS test_cart_total_applies_tax
The arranged values are named rather than inlined, so the assert can be read against them. Written as assert cart_total([10.0, 5.0], 0.10) == 16.5, the test would work identically and be harder to scan, because the situation and the action collapse into one line.
The single act line is the rule that matters most. One call means a failure can only have come from that one call, so there is nothing to narrow down. Two calls in the act phase would leave you wondering which one produced the wrong value.
The expected 16.5 is written as a literal rather than computed. Recomputing it as sum(prices) * 1.1 in the assert would make the test agree with the function's own logic and stop testing anything, which is a trap worth naming now.
Note that round(..., 2) in the function is doing real work, since 15 times 1.1 in floating point is 16.500000000000002. Without the rounding, this assert would fail.
An AAA test for a discount function
The same skeleton on a new function: two arranged variables, one call, one assert.
def apply_discount(price, percent): return round(price * (1 - percent / 100), 2) def test_apply_discount_takes_percent_off(): # Arrange price = 80.0 percent = 25 # Act result = apply_discount(price, percent) # Assert assert result == 60.0 test_apply_discount_takes_percent_off() print("PASS test_apply_discount_takes_percent_off")
Output
PASS test_apply_discount_takes_percent_off
Twenty-five percent off 80.0 is 80.0 times 0.75, which is 60.0, and working that out by hand is part of the exercise. An expected value derived from the function itself would test nothing, so the arithmetic has to come from outside the code.
The two arranged values are chosen to make that arithmetic easy. A price of 79.99 with 17 percent off would test the same behavior while forcing you to trust a calculator, and a test whose expected value you cannot verify at a glance is a test you will eventually edit to match the code.
The last two lines call the test and print the PASS line, which is the same manual approach lesson 1-3 replaced with a loop. Doing it by hand here keeps the focus on the test's shape.
An AAA test for a whitespace edge case
A single arranged string, one call, one assert. The interesting part is the input, which contains a run of three spaces.
def word_count(text): return len(text.split()) def test_word_count_ignores_extra_spaces(): # Arrange text = "hello world" # Act count = word_count(text) # Assert assert count == 2 test_word_count_ignores_extra_spaces() print("PASS test_word_count_ignores_extra_spaces")
Output
PASS test_word_count_ignores_extra_spaces
split() with no argument treats a run of whitespace as a single separator, so the count really is 2. Calling split(" ") instead would return five items, three of them empty strings, and the test would fail with a count of 5. That difference is exactly the behavior this test exists to lock down.
The test name says what the input is unusual about, and that is what makes it worth having alongside an ordinary two-word case. A name like test_word_count would leave a reader guessing why the string looked odd.
Naming the arranged variable text rather than inlining the string also has a practical benefit, since the triple space is easy to miss inside a function call and slightly easier to notice on its own line.