
Testing and Debugging Cheat Sheet
Welcome back, Python enthusiast! Whether you're just starting your testing journey or looking to sharpen your debugging skills, this comprehensive cheat sheet is here to guide you. Testing and debugging are essential parts of the development process—they help you write robust, reliable code and catch those pesky bugs before they reach production. Let's dive right in!
Writing Tests with unittest
Python's built-in unittest
module is a powerful tool for writing and running tests. It follows the xUnit style, which might be familiar if you've used testing frameworks in other languages. Here's a quick example to get you started:
import unittest
def add(a, b):
return a + b
class TestMathOperations(unittest.TestCase):
def test_add_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative_numbers(self):
self.assertEqual(add(-1, -1), -2)
if __name__ == '__main__':
unittest.main()
Run this script, and you'll see whether your tests pass or fail. The unittest.TestCase
class provides many assertion methods like assertEqual
, assertTrue
, assertRaises
, and more. Always structure your tests to cover both typical use cases and edge cases. This helps ensure your code behaves correctly under various conditions.
When organizing tests, it's common to have one test class per module or functionality you're testing. Each method in the class should test a specific aspect of your code. For instance, if you're testing a function that processes user input, you might have separate tests for valid input, invalid input, and boundary cases.
Assertion Method | Purpose |
---|---|
assertEqual(a, b) | Checks if a == b |
assertTrue(x) | Checks if x is True |
assertFalse(x) | Checks if x is False |
assertRaises(Exc, func) | Checks if func raises exception Exc |
assertIn(a, b) | Checks if a is in b |
assertIsNone(x) | Checks if x is None |
Remember to keep your tests isolated and independent. Each test should set up its own data and not rely on the state from previous tests. This prevents unexpected interactions and makes it easier to pinpoint failures.
pytest: A More Pythonic Approach
While unittest
is great, many developers prefer pytest
for its simplicity and powerful features. You can install it via pip (pip install pytest
), and it requires less boilerplate code. Here's the same test written with pytest:
def add(a, b):
return a + b
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, -1) == -2
Notice how there's no need to create a class—just write functions that start with test_
. pytest will automatically discover and run these tests. You can run them by simply executing pytest
in your terminal from the project directory.
pytest also supports fixtures, which are functions that run before your tests to set up resources. For example:
import pytest
@pytest.fixture
def sample_list():
return [1, 2, 3]
def test_list_length(sample_list):
assert len(sample_list) == 3
The sample_list
fixture provides a fresh list to any test that requests it as an argument. Fixtures help you avoid repetitive setup code and keep your tests clean and maintainable.
Another powerful feature of pytest is parameterized tests, which allow you to run the same test with multiple inputs:
import pytest
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, -1, -2),
(0, 0, 0),
])
def test_add_various_inputs(a, b, expected):
assert add(a, b) == expected
This runs test_add_various_inputs
three times with different sets of parameters, reducing duplication and making it easy to add new test cases.
Debugging with pdb
When tests fail, it's time to debug. Python's built-in debugger, pdb
, is your best friend for interactive debugging. You can insert a breakpoint in your code by adding import pdb; pdb.set_trace()
at the point where you want to start debugging. When the code reaches that line, it will pause and drop you into an interactive session.
For example:
def tricky_function(x):
result = x * 2
import pdb; pdb.set_trace() # Breakpoint here
return result + 1
print(tricky_function(5))
When you run this script, it will stop at the breakpoint, and you can inspect variables, step through code, and more. Here are some useful pdb commands:
- n (next): Execute the current line and move to the next one.
- s (step): Step into a function call.
- c (continue): Continue execution until the next breakpoint.
- l (list): Show the code around the current line.
- p (print): Print the value of an expression.
- q (quit): Quit the debugger.
In Python 3.7 and above, you can use the built-in breakpoint()
function instead of importing pdb. It does the same thing but is cleaner:
def tricky_function(x):
result = x * 2
breakpoint() # Modern breakpoint
return result + 1
Using pdb effectively can save you hours of frustration by letting you inspect your program's state at critical points.
Common Testing Scenarios
Let's look at some common scenarios you'll encounter when writing tests and how to handle them.
Testing functions that raise exceptions is important to ensure error conditions are handled properly. With unittest
, use assertRaises
:
import unittest
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)
With pytest, you can use pytest.raises
:
import pytest
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
Another common scenario is testing code that interacts with external resources, like databases or web APIs. Instead of hitting the real resource, which can be slow and unreliable, use mocking. The unittest.mock
module (or pytest-mock
for pytest) lets you replace parts of your system with mock objects.
Here's an example using unittest.mock
:
from unittest.mock import patch
import unittest
def get_data_from_api():
# Imagine this calls an external API
return "real data"
class TestApiCall(unittest.TestCase):
@patch('__main__.get_data_from_api')
def test_mock_api(self, mock_get):
mock_get.return_value = "mocked data"
result = get_data_from_api()
self.assertEqual(result, "mocked data")
The @patch
decorator replaces get_data_from_api
with a mock object for the duration of the test. You can set its return value or side effects to simulate different responses.
Mocking Method | Purpose |
---|---|
return_value | Set the value returned by the mock |
side_effect | Set exception or dynamic return values |
assert_called_once | Check if mock was called exactly once |
assert_called_with | Check arguments of the last call |
Mocking is especially useful for isolating the code under test from its dependencies, making your tests faster and more reliable.
Logging for Debugging
Sometimes, debugging with pdb isn't practical, especially in production environments. That's where logging comes in. Python's logging
module lets you add log messages to your code that can be turned on or off depending on the severity level.
Configure logging at the start of your application:
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
Then, add log statements throughout your code:
def complex_calculation(x, y):
logging.debug(f"Starting calculation with x={x}, y={y}")
result = x * y
if result < 0:
logging.warning("Result is negative")
logging.debug(f"Calculation result: {result}")
return result
By setting the log level to DEBUG
, you'll see all messages. In production, you might set it to WARNING
or higher to reduce noise. Logging provides a non-intrusive way to track your program's behavior without stopping execution.
You can also direct logs to different outputs, like files:
logging.basicConfig(filename='app.log', level=logging.DEBUG)
This is invaluable for post-mortem debugging when you need to understand what happened after a crash.
Handling Flaky Tests
Flaky tests are tests that sometimes pass and sometimes fail without any changes to the code. They erode trust in your test suite and should be fixed ASAP. Common causes include reliance on external services, timing issues, or shared state.
To avoid flaky tests:
- Isolate tests: Ensure each test sets up its own data and doesn't depend on others.
- Use mocking: Replace unreliable dependencies with predictable mocks.
- Avoid sleep calls: Instead of
time.sleep()
, use polling or events if waiting for a condition. - Run tests multiple times: Tools like
pytest-flakefinder
can run tests repeatedly to expose flakiness.
If you encounter a flaky test, try to reproduce it consistently. Add more logging around the failing area to understand what's happening. Sometimes, increasing timeouts or adding retries can help, but be cautious—this might mask underlying issues.
Debugging in IDEs
If you're using an IDE like PyCharm or VSCode, you have powerful graphical debugging tools at your disposal. These allow you to set breakpoints by clicking in the margin, inspect variables in a panel, and step through code with buttons instead of typing commands.
In PyCharm, for example, you can right-click and select "Debug" instead of "Run" to start a debugging session. You can then hover over variables to see their values or use the debug console to execute expressions.
IDE debuggers often provide a more intuitive experience than command-line pdb, especially for complex codebases. They also integrate with your test runner, letting you debug failing tests directly.
Testing Best Practices
To wrap up, here are some best practices to keep in mind as you write and maintain your tests:
- Write tests before or alongside code (Test-Driven Development can be very effective).
- Keep tests small and focused on one behavior.
- Use descriptive test names that indicate what is being tested.
- Run your tests frequently to catch regressions early.
- Aim for high code coverage but don't sacrifice quality for quantity.
- Refactor tests when you refactor code to keep them maintainable.
- Treat test code with the same care as production code.
Remember, the goal of testing is not just to find bugs but to prevent them. A good test suite gives you confidence to make changes and add features without breaking existing functionality.
Happy testing and debugging! May your code be ever bug-free and your tests always green.