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.