Why Testing Matters
Writing code is only half the job. The other half is proving that the code behaves as intended.
In production, the cost of a bug is not just a failing test — it is lost time, broken trust, and surprise outages. The best engineering teams treat testing as a default part of the workflow, not a heroic last-minute task.
A good mental model is this:
- Testing asks, “Did I break the expected behavior?”
- Debugging asks, “Why did this fail?”
They are related, but they are not the same.
Unit Tests and Assertions
A unit test checks one small piece of behavior in isolation. The structure is simple:
- arrange the data,
- invoke the function,
- assert the expected result.
Example in a generic form:
def test_addition():
assert add(2, 3) == 5
The purpose of an assertion is not to be dramatic — it is to encode a rule. If the requirement changes, the test changes with it. Over time, the test suite becomes a living specification.
Logging and Observability
Logs are the first layer of observability. They answer everyday questions such as:
- Did this code run at all?
- What input did it receive?
- What branch was taken?
- What value changed at this step?
Good logs are specific, structured, and useful at the moment of failure.
echo "starting job" >> app.log
python app.py 2>&1 | tee debug.log
A common mistake is logging everything without context. The better habit is to log the decisive facts: input, state transition, error class, and relevant identifiers.
Debugging from Symptoms to Root Cause
Debugging is a process of narrowing uncertainty. The best workflows are systematic:
- reproduce the problem,
- capture the exact symptom,
- isolate the smallest failing case,
- test a hypothesis,
- fix the root cause rather than the visible symptom.
A stack trace is often the first useful clue. A traceback or crash report tells you where the program was when it failed. Breakpoints and step-through debugging then let you inspect the observed state at that moment.
Breakpoints, Stepping, and Inspection
A debugger lets you pause execution and inspect the values that matter:
- local variables,
- function arguments,
- object state,
- call stack,
- and control flow.
This is especially powerful for issues that only happen under certain conditions or with certain inputs. A debugger improves the quality of your reasoning because you can test assumptions instead of guessing at them.
The Difference Between Debugging and Observing
A lot of “debugging” is actually just observing. The distinction matters:
- Observing means collecting information.
- Debugging means turning that information into a diagnosis.
A log line may tell you something happened. A debugger helps you understand why it happened. Together they form a complete workflow.
Practice Trajectory
- Write a small test for a function with a clear input and expected output.
- Add one assertion that fails on purpose, then fix it and re-run the test.
- Introduce a log line around a branch or state transition and verify it appears in the output.
- Reproduce a bug and inspect its traceback or stack trace.
- Use a debugger to pause at a line of code and inspect variable values before fixing the cause.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Catching regressions early | Tests give fast feedback before a change ships |
| Investigating a failure | Logs and traces reduce guesswork |
| Complex control flow | Breakpoints make hidden state visible |
| Production incidents | Good debugging habits turn chaos into a manageable investigation |