Testing and Debugging
Checking behavior against expectations, and finding why it differs. Reproduce, hypothesize, observe, fix, lock in with a test.
Level: intermediate
What is it?
Testing checks that code does what you expect; debugging finds out why it does not. A unit test exercises one function with chosen inputs; an integration test exercises pieces together. Debugging is the scientific method applied to code: reproduce the failure, form a hypothesis, observe (print statements, logs, a debugger), and confirm the fix with a test that would have failed before.
Why does it matter?
Code you cannot test cannot be changed safely. Tests turn 'I think it works' into a repeatable check, and a disciplined debugging routine turns hours of guessing into minutes of narrowing down.
Mental model
A doctor working a case. Symptoms (the failing behavior), a differential (hypotheses), tests that rule causes out, then a treatment and a follow-up to confirm the cure.
How it works
- A good test is small, fast, deterministic, and fails for one reason. Name it after the behavior it checks.
- Test the boundaries and the awkward cases: empty input, one element, maximum size, negative numbers, invalid data.
- Reproduce first. A bug you cannot trigger on demand cannot be verified as fixed.
- Bisect: halve the search space with a print, an assert, or git bisect until the fault is between two lines or two commits.
- Read the stack trace from the bottom of your own code upward. The first frame in your code is usually where to look.
- After the fix, add a regression test so the same bug cannot return quietly.
Steps
- Reproduce the failure
- Form a hypothesis
- Observe with a print or debugger
- Narrow to a line
- Fix and add a regression test
Code example: A test that pins down the edge cases
def is_leap(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
def test_is_leap():
assert is_leap(2024)
assert not is_leap(1900) # divisible by 100 but not by 400
assert is_leap(2000) # divisible by 400
assert not is_leap(2023)
test_is_leap()
print("ok")Real-world usage
- Continuous integration that blocks broken changes
- Refactoring with a safety net
- Investigating production incidents from logs and traces
Common mistakes
- Changing code at random until the symptom disappears, without understanding the cause.
- Tests that depend on the clock, the network, or the order they run in.
- Fixing the bug but not adding a test, so it comes back.
Interview questions
What makes a good unit test?
It is fast, deterministic, isolated from other tests, checks one behavior, and fails with a message that tells you what broke.
How do you debug a bug you can only reproduce sometimes?
Capture more information (logs, seeds, inputs) at the failure, look for shared state, timing, or randomness, try to make it deterministic (fix seeds, control the scheduler), and shrink the input until the failure is small and reliable.
What is a regression test?
A test added after fixing a bug that reproduces the original failure. It fails on the old code and passes on the fix, so the bug cannot silently return.