Good tests catch bugs before they reach users. Good debugging finds the root cause instead of the symptom. But bad habits in testing and debugging waste time, create false confidence, and introduce new bugs while trying to fix old ones.
This article covers the most common mistakes Python developers make when testing and debugging, drawn from real code reviews and postmortems. Each mistake is avoidable once you know what to look for.
Tests without assertions
A test method that calls a function and does nothing with the result always passes. It exercises the code but verifies nothing. This is the single most common testing mistake, and it creates false confidence in code that may be completely broken.
def test_calculate_total(self):
calculate_total([10, 20, 30]) # No assertion!Fix it by adding at least one assertion that checks a specific expected outcome, so the test fails loudly the moment the function stops behaving correctly:
def test_calculate_total(self):
result = calculate_total([10, 20, 30])
self.assertEqual(result, 60)The same mistake happens with tests that only check that no exception was raised. Not crashing is the minimum requirement, not the definition of correct behavior.
Testing implementation details
A test that checks how a function works instead of what it produces breaks when you refactor. If your test asserts that a specific private method was called with specific arguments, any restructuring of the internal logic causes a test failure even if the public behavior is unchanged.
Write tests against the public interface. Given these inputs, the function should return this output.
Do not test which internal helpers were called or in what order. Those are implementation details that should be free to change.
Tests for mock objects are especially prone to this mistake. Verify that your code called an external dependency with the right logical arguments. Do not verify the exact sequence of method calls on a complex mock chain unless that sequence is part of the contract.
Catching exceptions too broadly
A try-except block that catches Exception hides every error, including programming mistakes like NameError and AttributeError. These are errors you want to surface during testing, not swallow silently.
# Bad: catches everything
try:
process_data(file_path)
except Exception:
passCatch only the exceptions you expect and know how to handle, and let everything else propagate so it surfaces as a visible failure:
# Good: catches specific, expected errors
try:
process_data(file_path)
except (FileNotFoundError, PermissionError) as e:
log_error(f"Cannot process {file_path}: {e}")When writing tests with assertRaises, specify the exact exception type. Using Exception as the expected type passes for any error, including errors unrelated to what you intended to test.
Debugging by random guessing
When a test fails or a program crashes, the temptation is to change something and run it again: change a variable name, add a type conversion, reorder a few lines. If it works, move on.
This is the slowest possible debugging strategy, and it often fixes the symptom without understanding the cause.
Systematic debugging follows a different process. Form a hypothesis about what is wrong, then design a test that would confirm or disprove it.
Run the test. If the hypothesis is wrong, form a new one based on what you learned.
This process sounds formal but takes less time in practice than guessing. A single print statement or debugger inspection that confirms a hypothesis eliminates an entire category of possible causes. Three or four cycles of this process typically find the root cause.
Not using the debugger
Print statements work for simple bugs, but they become inefficient when the bug is buried in a loop, a nested function, or a rarely-triggered conditional. Each print cycle takes time: add print, run code, read output, add another print.
Python's built-in debugger, pdb, and VS Code's visual debugger let you pause execution at any line, inspect every variable, and step through code one line at a time. The article on debugging Python code with pdb covers the workflow in detail.
The time investment to learn the debugger pays back within a few debugging sessions. What takes ten print cycles takes one breakpoint and three steps in the debugger.
Over-mocking in tests
A test that mocks every dependency is testing mocks, not your code. When every external call is replaced with a mock that returns a hardcoded value, the test passes regardless of whether the real integration would work.
Mock at the boundaries of your system. Replace the HTTP call, the database query, or the file system operation. Do not replace utility functions, data transformation logic, or anything you wrote yourself unless it has side effects that make testing impractical.
Ignoring test failures
A test that fails intermittently and gets ignored because "it passes when I run it again" is a bug in your test suite. Flaky tests erode trust in the entire suite. When the test runner shows red, developers learn to ignore it, and real failures get missed.
Fix flaky tests immediately. Common causes include tests that depend on execution order, tests that rely on real time or random values, and tests that share mutable state. Isolate the cause and either fix the test or the code it exercises.
The next article covers testing best practices for Python projects, which builds on avoiding these mistakes and adds positive patterns for building a reliable test suite.
Rune AI
Key Insights
- Every test must contain an assertion; a test without an assertion always passes and provides no value.
- Test the public interface of your code, not internal implementation details that change during refactoring.
- Catch specific exception types, not broad Exception, so unexpected errors surface during testing.
- Use assertRaises to verify exceptions instead of try-except inside tests.
- Debug systematically by forming a hypothesis and testing it, not by changing code randomly until it works.
Frequently Asked Questions
What is the most common mistake in Python unit tests?
Why is testing implementation details a mistake?
What is the problem with catching Exception broadly in try-except blocks?
Conclusion
Most testing and debugging mistakes come from the same root: treating these activities as afterthoughts rather than core programming skills. Write assertions that check specific outcomes. Test behavior, not implementation. Debug systematically instead of guessing. These habits take practice, but they pay back the investment every time a test catches a regression.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.