Testing, 1-2-3: Getting Started Debugging Python
By Magnus Lie Hetland
How do you know that your program works? Can you rely on yourself to write flawless code all the time? Meaning no disrespect, I would guess that’s unlikely. It’s quite easy to write correct code in Python most of the time, certainly, but chances are your code will have bugs.
Debugging is a fact of life for programmers—an integral part of the craft of programming. However, the only way to get started debugging is to run your program. Right? And simply running your program might not be enough. If you have written a program that processes files in some way, for example, you will need some files to run it on. Or if you have written a utility library with mathematical functions, you will need to supply those functions with parameters in order to get your code to run.
Programmers do this kind of thing all the time. In compiled languages, the cycle goes something like “edit, compile, run,” around and around. In some cases, even getting the program to compile may be a problem, so the programmer simply switches between editing and compiling. In Python, the compilation step isn’t there—you simply edit and run. Running your program is what testing is all about.
Test First, Code Later
To plan for change and flexibility, which is crucial if your code is going to survive even to the end of your own development process, it’s important to set up tests for the various parts of your program (so-called unit tests). It’s also a very practical and pragmatic part of designing your application. Rather than the intuitive “code a little, test a little” practice, the Extreme Programming crowd has introduced the highly useful, but somewhat counterintuitive, dictum “test a little, code a little.”
In other words, test first and code later. This is also known as test-driven programming. While this approach may be unfamiliar at first, it can have many advantages, and it does grow on you over time. Eventually, once you’ve used test-driven programming for a while, writing code without having tests in place may seem really backwards.
Precise Requirement Specification
When developing a piece of software, you must first know what problem the software should solve—what objectives it should meet. You can clarify your goals for the program by writing a requirement specification, a document (or just some quick notes) describing requirements the program must satisfy. It is then easy to check at some later time whether the requirements are indeed satisfied. But many programmers dislike writing reports and in general prefer to have their computer do as much of their work as possible. Here’s good news: you can specify the requirements in Python and have the interpreter check whether they are satisfied!
The idea is to start by writing a test program and then write a program that passes the tests. The test program is your requirement specification and helps you stick to those requirements while developing the program.
Let’s take a simple example. Suppose you want to write a module with a single function that will compute the area of a rectangle with a given height and width. Before you start coding, you write a unit test with some examples for which you know the answers. Your test program might look like the one in Listing 1.
Listing 1. A Simple Test Program
from area import rect_area
height = 3
width = 4
correct_answer = 12
answer = rect_area(height, width)
if answer == correct_answer:
print('Test passed ')
print('Test failed ')
In this example, I call the function rect_area (which I haven’t written yet) on the height 3 and width 4 and compare the answer with the correct one, which is 12.
If you then carelessly implement rect_area (in the file area.py) as follows and try to run the test program, you would get an error message.
def rect_area(height, width):
return height * height # This is wrong ...
You could then examine the code to see what was wrong and replace the returned expression with height * width.
Writing a test before you write your code isn’t just a preparation for finding bugs—it’s a preparation for seeing whether your code works at all. It’s a bit like the old Zen koan: “Does a tree falling in the forest make a sound if no one is there to hear it?” Well, of course it does (sorry, Zen monks), but the sound doesn’t have any impact on you or anyone else. With your code, the question is, “Until you test it, does it actually do anything?” Philosophy aside, it can be useful to adopt the attitude that a feature doesn’t really exist (or isn’t really a feature) until you have a test for it. Then you can clearly demonstrate that it’s there and is doing what it’s supposed to do. This isn’t only useful while developing the program initially but also when you later extend and maintain the code.
Planning for Change
In addition to helping a great deal as you write the program, automated tests help you avoid accumulating errors when you introduce changes, which is especially important as the size of your program grows. You should be prepared to change your code, rather than clinging frantically to what you have, but change has its dangers. When you change some piece of your code, you very often introduce an unforeseen bug or two. If you have designed your program well (with appropriate abstraction and encapsulation), the effects of a change should be local and affect only a small piece of the code. That means that debugging is easier if you spot the bug.
The 1-2-3 (and 4) of Testing
Before we get into the nitty-gritty of writing tests, here’s a breakdown of the test-driven development process (or at least one version of it):
- Figure out the new feature you want. Possibly document it and then write a test for it.
- Write some skeleton code for the feature so that your program runs without any syntax errors or the like, but so your test still fails. It is important to see your test fail, so you are sure that it actually can fail. If there is something wrong with the test and it always succeeds no matter what (this has happened to me many times), you aren’t really testing anything. This bears repeating: see your test fail before you try to make it succeed.
- Write dummy code for your skeleton, just to appease the test. This doesn’t have to accurately implement the functionality; it just needs to make the test pass. This way, you can have all your tests pass all the time when developing (except the first time you run the test, remember?), even while initially implementing the functionality.
- Rewrite (or refactor) the code so that it actually does what it’s supposed to, all the while making sure that your test keeps succeeding.
You should keep your code in a healthy state when you leave it—don’t leave it with any tests failing (or, for that matter, succeeding with your dummy code still in place). Well, that’s what they say. I find that I sometimes leave it with one test failing, which is the point at which I’m currently working, as a sort of “to-do” or “continue here” for myself. This is really bad form if you’re developing together with others, though. You should never check failing code into the common code repository.
About the Author
Magnus Lie Hetland is an experienced Python programmer, having used the language since the late 1990s. He is also an associate professor of algorithms at the Norwegian University of Science and Technology, having taught algorithms for the better part of a decade. Hetland is also the author of Python Algorithms, as well as several scientific papers.
Want more? This article is excerpted from Beginning Python, Third Edition (2017). Get your copy today and gain a fundamental understanding of Python’s syntax and features with this up–to–date introduction and practical reference.