Lesson 6 · Testing Without a Robot
Lesson 6 · Testing Without a Robot
Teacher mode is on. Toggle it off (bottom-right) to preview the student view.
Your Manhattan class is pure math — no motors, no sensors. That's a gift: you can
prove it's correct on any computer before the robot ever moves. This lesson builds the
habit real software engineers live by: test on screen before you test on the floor.
Learning Objectives
By the end of this lesson you will be able to:
- Explain why testing on screen is faster than debugging on the robot
- Describe separation of concerns — planning (math) vs. driving (hardware)
- Hand-calculate an expected path, then compare it to the actual output
- Write a reusable
run_test()helper and a suite of test cases - Debug a failing test with print statements
Why test without the robot?
Would a pilot fly a brand-new plane without testing the engines on the ground? Of course not. The same logic applies to your code. A robot driving into a wall tells you something is wrong — but not what or where. A failing test on screen tells you the exact input that produced the wrong output.
This works because of separation of concerns: the Manhattan class only computes
a path, and the Navigator (coming in Lesson 8) drives it. Each can be checked on its
own:
| Problem | Where it runs | What you test |
|---|---|---|
| Computing the path | Any computer | Does the path make sense? |
| Driving the path | On the robot | Does the robot follow it? |
Because compute_path is just math, you can run it hundreds of times in seconds — no
grid, no batteries, no chasing a robot across the room.
What does 'separation of concerns' let you do here?
Expected vs. actual
The heart of testing: decide what the answer should be by hand, then check whether the code agrees. Hand-trace (0,0) → (2,3) — down 2 rows, right 3 columns — to get the expected path, then compare:
expected = [(1, 0), (2, 0), (2, 1), (2, 2), (2, 3)]
manhattan = Manhattan((0, 0))
actual = manhattan.compute_path((2, 3))
if actual == expected:
print("Test 1: PASS")
else:
print("Test 1: FAIL")
print(" Expected:", expected)
print(" Actual: ", actual)
The expected list has to come from your own tracing. If you just copy whatever the code prints and call it "expected," you haven't tested anything — you've assumed the code is right.
Where should the 'expected' path come from?
Activity · A reusable run_test() helper
That compare-and-report pattern repeats for every case, so wrap it in a function:
def run_test(test_name, start, dest, expected):
manhattan = Manhattan(start)
actual = manhattan.compute_path(dest)
if actual == expected:
print(test_name, "- PASS")
else:
print(test_name, "- FAIL")
print(" Expected:", expected)
print(" Actual: ", actual)
Now each test is a single line — and a good suite covers every direction and every edge case. Here's one case worked out, and the five your suite still needs:
run_test("Forward right", (0, 0), (2, 3), [(1,0), (2,0), (2,1), (2,2), (2,3)])
run_test("Backward left", (2, 3), (0, 0), [ ... ]) # trace it, then fill it in
run_test("Same row", (1, 0), (1, 3), [ ... ])
run_test("Same column", (0, 2), (3, 2), [ ... ])
run_test("Same spot", (1, 1), (1, 1), [ ... ])
run_test("One step", (0, 0), (0, 1), [ ... ])
Work each expected out by hand before you run anything. That is the whole
point: a test whose expected value came from the program's own output proves
nothing except that the program agrees with itself. If a test fails, one of two
things is wrong — your trace or your code — and finding out which is the skill.
If you only ever test (0,0) → (2,3), a bug that only shows up going backward will slip right past you.
Your test shows actual = [(1,0),(2,0),(2,1)] but expected = [(1,0),(2,0),(2,1),(2,2),(2,3)]. What's the likely problem?
Activity · Debug a failing test
When a test fails, don't guess — add print statements inside compute_path to watch the
logic run:
def compute_path(self, destination):
print("Computing from", self.position, "to", destination)
path = []
current_row, current_col = self.position
dest_row, dest_col = destination
while current_row < dest_row:
current_row = current_row + 1
path.append((current_row, current_col))
print(" added:", (current_row, current_col))
# ... the other three loops, each printing what they add
return path
The trace shows exactly which loop ran and what it appended. Once you've found and fixed
the bug, remove the debug prints so the output stays clean. (Try it: change one + 1
to - 1 and watch a test catch the bug.)
After using print statements to fix a bug, what should you do with them?
Real-world connections
"Verify before you deploy" is everywhere in engineering:
Unit tests
Professional code ships with automated tests that check each function against known answers — a formal version of run_test().
Simulation
Rockets and planes fly thousands of simulated missions before a real one, catching problems on screen.
Digital twins
Self-driving systems test on virtual roads first — far cheaper and safer than a real crash.
Wrap-up
- Why test on screen before the robot? (Faster, and it points to the exact broken input.)
- Where does the expected path come from? (Hand-calculation, before you run the code.)
- What makes a good test suite? (Every direction and edge case, not just one happy path.)