Skip to main content

Lesson 6 · Testing Without a Robot

Module 4 · Manhattan Navigation

Lesson 6 · Testing Without a Robot

50 minPython testingTest on screen first
👩‍🏫

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:

ProblemWhere it runsWhat you test
Computing the pathAny computerDoes the path make sense?
Driving the pathOn the robotDoes 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.

Knowledge Check

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.

Knowledge Check

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.

Knowledge Check

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.)

Knowledge Check

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:

Software

Unit tests

Professional code ships with automated tests that check each function against known answers — a formal version of run_test().

Aerospace

Simulation

Rockets and planes fly thousands of simulated missions before a real one, catching problems on screen.

Cars

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.)

Resources