There are two types of engineers in this
universe. The first type writes code, then walks
away and does not watch it run. They just assume everything will go according to
plan. Then there's the second type: those who write unit tests. With Python's
built-in unit test module, you can easily write code to test your software. This
will give you a peace of mind which eludes all too many programmers. To quote Shakespeare, "Trust but verify...
with unit testing." Suppose you ask an engineer to
write a function that computes the area of a circle. They get excited, because it
sounds like an easy task. The engineer creates a file called circles.py and
gets to work. First, they import the constant pi from the math module. Next,
they define the function. And with a single line they compute and return the
area. Finally, they give us the function to test... then strut back to their
workstation. Let's roll up our sleeves and take a look. I do not see a doc
string, so I assume this function will work with any input. Let's test the
values 2, 0, negative 3, 2 plus 5j, true, and the string "radius." Don't forget that "j" is
the square root of negative 1 in Python. I know. This makes me itch, too. For each
radius, I will display this formatted message. We loop over the list of radii,
compute the area, then print the test message. To format the message. call the
format method and pass in the values for the two named fields. Run. The area is
correct for a circle of radius 2... and the area of a circle with radius 0 is indeed 0 ... But wait! What's this? The function computes the area of a circle with a
negative radius. That's regrettable. It gets worse. The function returns a
complex area for a circle with a complex radius. Hoo-boy. And the area of a circle with radius true is pi? You have got to be
kidding me. Thankfully, the function gives an error
when you try to find the area of a circle with a string as a radius. My conclusion is that the function is... how shall I say this politely... a grave
disappointment. Rather than simply criticizing the engineer's work, we will now write unit tests to check that the function works properly. The engineer
will then be able to test their code before submitting it for review. The
function is in a file called circles.py You typically put the unit tests in a
separate file. There are two common conventions for naming the test module.
The first is to call it test_circles.py where you put a test
underscore before the name of the module you are testing. The second is to name it
circles_test.py where you put an underscore test
after the name of the module In the first case, all the test modules will be grouped together. In the
second case, each module appears next to its test class in your file system. It is
up to you or your team to choose which naming convention to use. By the way, some
people will put tests in a separate folder entirely, but for simplicity we
will keep all classes and their unit tests in the same folder. And for naming,
we will follow the first convention. Create a file called test_circles.py When writing unit tests, the first thing to do is import the unit
test module. If we want to test the circle area function then we must first
import it. And to check the answers, we will also need to import the number pi.
Next create a class that is a subclass of the test case class in the unit test
module. We will call our class "TestCircleArea."
A more descriptive name, I cannot imagine. We will write our test methods inside this class. Each test
method must start with the word test. The first will be test_area.
Here, we will check that the function correctly computes the areas of several
circles. To do this, we will call the assert almost equal method. The first
value will be the output of the circle area function and the second value will
be the correct answer. The Python unit test framework will compare these two
values, and if they are correct to seven decimal places it will assume they are
equal. Let us also check the function works for a circle of radius 0 and a
circle our radius 2.1 If any of these comparisons fail, then
Python will register that the test area method failed. To run this unit test,
open a shell and go to the directory containing both the circles module and
test circles module. To run the unit tests, enter Python - M unit test and
then the name of the test module test_circles. The - M option instructs Python
to run the unit test module as a script. Run. We see the test ran, and everything
is OK. By the way, you can also simply run Python - M unit test. Python will then
use a process called test discovery where it will search for tests and run
them. A handy shortcut, indeed. Let us now test the function to see if it handles
improper inputs correctly. We will write a new test method called test values to
see if the function raises a value error when the input is a negative number. To
check that an exception is raised, you use the assert raises method. The first
argument is the exception class that should be raised. The second argument is
a function and the remaining inputs are arguments to the function. In this case,
if we try to compute the area of a circle with radius negative two, the
function should raise a value error. Now, return to the console and run the unit
tests. This time, we get a screen full. Let us count the ways Python indicates there
is a problem. There is an F for fail; the word fail in all caps; the word failed;
and at the bottom the number of failures. You've made your point, Python. As we saw
earlier, a value error was not raised when the input was negative.
Let us return to the circles module. As the unit test indicated, if the radius is
negative, we need to raise a value error. So before computing the area, we check to
see if it is negative. If so, raise a value error with a helpful error message.
If not, then compute and return the area. Return to the console, and run the unit
tests once more. okay Just okay? Python is stingy with praise. So far, we have seen two assert methods: assert almost equal and assert raises.
Python has many more methods for unit testing. There are dozens and dozens of
methods. One way to learn about a particular assert method is by looking
at the help text in interactive mode. For example, suppose you want to learn more
about the assert set equal method. To see the help text, import the unit test
module. Then use the help function on the method. We first enter the module unit
test, then the class test case, then the method name assert set equal. Python
gives us a nice, detailed description of this assert method. Let us return to our
example, since we have unfinished business. We also want to make sure the
function raises a type error whenever the input is not a real number, so we
will add a third test method to our unit test. This test will check that a type
error is raised when the input is not a real number.
Let us check that an exception is raised when the input is a complex number... a
boolean... or a string. If we run the unit tests again, we are alerted to our
failures. We can fix this by returning to the circle area function. To address this
problem, we first check the type of the input. If the type is not an integer or a
float, then we will raise a type error. The function is looking much better.
Now, run the unit tests once more. Everything is OK. Unit tests save the day. Change is one constant in programming.
Languages evolve. Egineers may come and go. but if you use unit tests then you
will be more confident about upgrading and improving existing code without
causing problems for others. So do not fear tests... embrace them. To quote
Franklin Roosevelt, the only thing we have to fear is a lot of failure
messages when running our unit tests.