Your first tests · Software Testing
How do you know your code still works after you change it?
Course contentsWhy you need it
Every change can break something that used to work, often far from the line you touched. Clicking through the app by hand after every change is slow and you'll miss things. Automated tests check it for you in seconds, every time, and they're the reason teams can change big codebases without fear.
The intuition
A test is a small function that runs your code and checks the result with assert:
def test_add():
assert add(2, 3) == 5assert condition does nothing when the condition is true and raises AssertionError when it's false. Add a message to say what went wrong: assert got == 5, f"got {got}".
Most tests follow three steps: arrange (set up the input), act (call the code), assert (check the result).
A test runner finds every function whose name starts with test_, runs it, and reports which passed. In real projects that's pytest:
$ pytest
test_shop.py ..F [100%]
FAILED test_shop.py::test_extra_spacesHere we use a ten-line runner that does the same job.
A good test fails when the code is wrong. A test that passes no matter what checks nothing.
Definition
assert
A statement that raises AssertionError when its condition is false. Tests are built from asserts.
Test runner
A tool that finds test functions, runs them and reports passes and failures. pytest is the usual one for Python.
Regression
Something that used to work and broke after a change. Tests exist mainly to catch regressions.
Examples
assert
Try it yourselfSilent when true, an error when false.
def add(a, b):
return a + b
assert add(2, 3) == 5
print("first check passed")
try:
assert add(2, 2) == 5, "2 + 2 should not be 5"
except AssertionError as e:
print("failed:", e)first check passed failed: 2 + 2 should not be 5
- L4True, so nothing happens and the program carries on.
- L8False: AssertionError with our message.
- L10We catch it here only to show the message; a test runner does this for you.
failed: 2 + 2 should not be 5
Three tests and a runner
Try it yourselfEach test checks one behaviour; the runner reports them all.
def slugify(title):
return title.lower().replace(" ", "-")
def test_simple():
got = slugify("Hello World")
assert got == "hello-world", f"got {got!r}"
def test_already_lower():
got = slugify("abc")
assert got == "abc", f"got {got!r}"
def test_extra_spaces():
got = slugify(" Hi there ")
assert got == "hi-there", f"got {got!r}"
def run(tests):
passed = 0
for test in tests:
try:
test()
print("PASS", test.__name__)
passed += 1
except AssertionError as e:
print("FAIL", test.__name__, "-", e)
print(f"{passed}/{len(tests)} passed")
run([test_simple, test_already_lower, test_extra_spaces])PASS test_simple PASS test_already_lower FAIL test_extra_spaces - got '--hi--there-' 2/3 passed
- L1The code under test: turn a title into a URL part.
- L13An awkward input: spaces at both ends and doubled.
- L23A failing test doesn't stop the others.
- L27Two pass. The third shows exactly what came back.
Common mistakes
Tests that can't fail
assert slugify("a") passes for any non-empty result. Compare with the exact expected value.
Testing too much in one test
When a test with ten asserts fails, you don't know which behaviour broke. One behaviour per test, with a name that says which.
Only testing the happy path
Bugs hide in the awkward cases: empty input, extra spaces, zero, the biggest value. The next lesson is about finding them.
Quick quiz
1. What does `assert total == 10` do when total is 10?
Show the answer
Answer: A
assert only speaks up when the condition is false.
2. Which test is the most useful?
Show the answer
Answer: A
Only the exact comparison fails when the calculation is wrong.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictRead the report
Type exactly what this prints.
- ModifyMake the tests pass
test_extra_spacesfails. Fixslugify(not the test) so all three tests pass: extra spaces anywhere should disappear, and words are joined by single dashes. - WriteTests that catch bugs
discount(price, percent)takes a percentage off a price and rounds to cents. Below it are three broken versions. Writetest_functions so that they pass ondiscountbut fail on each broken version.passes(version)runs your tests against a version; the grader calls it on the real one, the broken ones, and one more broken version you can't see. The one test you have now catches nothing.
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| assert | assert 断言 | Raises AssertionError when its condition is false. |
| Unit test | 单元测试 | A small automated test of one piece of code. |
| pytest | pytest | The most common test runner for Python. |