An Introduction to the Art of Computer Programming Using Python in the Age of Generative AI

XIX. Testing and Debugging

Introduction

Computer scientist Edsger Dijkstra, a pioneer of structured programming, wrote in 1970: "Program testing can be used to show the presence of bugs, but never to show their absence!" Testing is essential because it helps confirm that your code behaves correctly under a variety of circumstances, while significantly reducing the likelihood of undiscovered defects. Although no amount of testing can prove that your program is 100% bug-free, maintaining a solid testing strategy is a cornerstone of professional software development.

Black-Box Testing

Black-box testing involves validating a piece of software solely by examining its inputs and outputs, without direct knowledge of its internal code or structure. This approach focuses on functional requirements, ensuring the software produces correct results for a range of test cases and edge conditions. Because the tester doesn’t look inside the code, black-box testing is excellent for validating user-facing behavior and verifying compliance with specifications.

White-Box Testing

By contrast, white-box testing (sometimes called clear-box or glass-box testing) involves examining the software’s internal logic and code paths. Testers or developers use knowledge of the implementation details—such as specific functions, branches, and loops—to create tests that ensure each pathway in the code is exercised. White-box and black-box approaches often complement each other to provide broader coverage and assurance of software quality.

Using Debuggers

Debugging is the process of locating, diagnosing, and fixing errors within your code. Python’s built-in debugger, pdb, is a powerful tool for stepping through your program line by line, inspecting variables, and testing hypotheses about where the bug might be. Breakpoints let you pause execution at a specific point so you can investigate the current state of variables or step through the next lines of code.


# To use PDB, simply import the module and add the line pdb.set_trace()
# at the location where you want to start debugging.
import pdb

def add_numbers(a, b):
    pdb.set_trace()
    return a + b

result = add_numbers(1, 2)
print(result)
        

In the above snippet, we place pdb.set_trace() inside the function add_numbers so that execution will pause right before the line return a + b. When you run this script, you’ll see the prompt (Pdb), where you can use commands to inspect or manipulate the debugging session:

By examining variable values, you can confirm that a is 1 and b is 2 before stepping to the line that returns their sum. This might seem trivial for a simple function like add_numbers, but in larger, more complex programs, strategic breakpoints let you trace exactly how data changes, making it far easier to find and fix logic errors.

Writing Unit Tests

Unit tests validate the functionality of small, isolated parts (or “units”) of code. Python’s built-in unittest module offers test case classes, setup routines, and assertion methods that make it straightforward to implement repeatable tests. When writing unit tests, focus on verifying logic within individual functions or classes. For a more flexible or minimalist testing style, many developers also use the pytest library, which allows for simpler test functions and extensive plugins.


import unittest

def multiply(a, b):
    return a * b

class TestMultiplication(unittest.TestCase):
    def test_multiply(self):
        self.assertEqual(multiply(2, 3), 6)
        self.assertEqual(multiply(-1, 3), -3)
        self.assertEqual(multiply(0, 3), 0)

if __name__ == '__main__':
    unittest.main()
        

Test-Driven Development (TDD)

Test-Driven Development (TDD) is a process where you write unit tests before writing the code that satisfies those tests. It commonly follows these steps:

This cycle helps you produce code that is well-defined, fits the requirements, and remains free of regression issues as you continue developing. TDD also encourages good design practices by forcing developers to think about how their components should behave before coding them.

Back to Home