Using unittest
to test your Python code
Test-driven development involves writing “unit tests” for each individual unit of code: typically each function definition.
Unit tests, or “test cases” as they are sometimes called, are specified by:
- Writing a function call with an actual parameter (the actual result)
- Writing the value what we expect that function to return (the expected results)
- Asserting that those are equal to each other, i.e.
actual==expected
Then, if actual==expected
, the test cases passes, otherwise it fails.
In the case of tests involving decimals, we might instead assert that the actual and expected are almost equal, meaning that the absolute value of the difference between them is less than some small value—or to say it another way, that they are equal within a certain number of decimal places.
One way to write these test cases in Python is with a module that we can
import called unittest
.
As an example, consider this simple function that converts Fahrenheit
to Celsius, stored in tempFuncs.py
# tempFuncs.py
def ftoc(fTemp):
return (fTemp - 32)*(5.0/9.0)
To write tests for this, we can put the following into a file called
test_tempFuncs.py
. (Note that we don’t have to call the file test_
followed by the name of the file we are testing, but that is a handy naming convention to follow.)
Following the code block, we provide an explanation one line at a time.
# test_tempFuncs.py
import unittest
from tempFuncs import ftoc
class Test_tempFuncs(unittest.TestCase):
def test_ftoc_1(self):
self.assertAlmostEqual(ftoc(212.0),100.0)
def test_ftoc_2(self):
self.assertAlmostEqual(ftoc(32.0),0.0)
def test_ftoc_3(self):
self.assertAlmostEqual(ftoc(-40.0),-40.0)
def test_ftoc_4(self):
self.assertAlmostEqual(ftoc(67.0),19.4444,places=3)
if __name__=="__main__":
unittest.main()
Line-by-line explanation
We now explain the contents of test_tempFuncs.py
one or two lines at a time.
# test_tempFuncs.py
This line is merely a comment with the name of the file, for identificatio purposes.
import unittest
We must import unittest
to be able to use the features provided in this module for unit testing.
from tempFuncs import ftoc
This line imports the function we want to test, ftoc
, directly from the file tempFuncs.py
. We could also have written import tempFuncs
, but then we’d have to write tempFuncs.ftoc
each time we wanted to use the ftoc
function.
class Test_tempFuncs(unittest.TestCase):
This first word on the line, class
, indicates that what follows is a
class
definition. The name of the class is Test_tempFuncs
, and the
part in parenthesis, unittest.TestCase
indicates the parent class of
this one. This class, Test_tempFuncs
will inherit some features from
its parent, that makes it function properly as a TestCase.
The concept of a class
is one that we’ll get into more
deeply later when we discuss object-oriented programming, and which
you’ll explore much more deeply when you take CSE 8A or 11 in Java.
For now, it is enough to know two things—although this is vastly oversimplified explanation and far from the full truth about what a class is:
- Know that a class is a collection of related function definitions.
- Know that classes can inherit some capabilities from a parent class, so that we don’t have to write all of the code they need to do some job.
In this case, all of the related function definitions are the ones that
immediately follow the line that starts with class ...
, and are indented
under it. The class ends with the blank line just before the if
test,
which is not indented. The fact that the if
test is not indented
indicates that it is the first line that is NOT part of the class.
Now we turn to the first function definition that is part of the class:
def test_ftoc_1(self):
self.assertAlmostEqual(ftoc(212.0),100.0)
In Python, each function that is part of class typically has, as its first
parameter, a special value called self
. For now, it is enough to know
that this special value has to be there, and it gives us access to some of
the special feature we “inherit” from our parent.
So, when we want to check whether ftoc(212.0)
converts the Farenheit value
212.0 correctly to the Celsius value 100.0
, we write:
self.assertAlmostEqual(ftoc(212.0),100.0)
We put this inside the function called test_ftoc_1
, and we put that
function definition inside a class that inherits from unittest.TestCase.
We then add two more similar function definitions, for two additional test cases. Each one needs its own separate name.
def test_ftoc_2(self):
self.assertAlmostEqual(ftoc(32.0),0.0)
def test_ftoc_3(self):
self.assertAlmostEqual(ftoc(-40.0),-40.0)
Up until now, we expect the values to be either exactly equal, or very close
to exactly equal. The assertAlmostEqual
function, by default, compares
the absolute value of the difference to seven decimal places. If we want to
compare to fewer or more, though we can override the default, as shown
in the last function definition of our sample code:
def test_ftoc_4(self):
self.assertAlmostEqual(ftoc(67.0),19.4444,places=3)
The class is now finished, and we have just two more lines of code. This first line of code is peculiar, but you’ll see it a lot, and eventually get used to it:
if __name__=="__main__":
unittest.main()
The if test here is indeed strange. What is __name__
? and what is "__main__"
? The answer is that this is a bit of a hack, a kludge—that is, a sort of inelegant solution to a problem. In Python, a file can be run in at least two different ways:
-
It can be “directly run”—that is, a user asked for this file by name to be run by selecting the “Run” menu option in idle, for example, or by typing
python thisFile.py
at the unix command line. -
It can be “indirectly run” by being imported into another Python file that is directly run. (That can also be several layers deep: it may be imported by a file, that is imported by a file, that is imported by a file … that is directly run.)
Sometimes we want to know which one is happening, and run some code ONLY in the case where the file is being “directly run”.
That is what the line of code `if name == “main” does.
Any time a file is being directly run, the __name__
variable gets set to
the special value "__main__"
.
Any other time, it is set to the name of the module, which is the name
of the Python file without the .py
on the end (e.g. for foo.py
,
name is set to foo
.
So in the end, the if test below just means “do this code only if this file is being directly run; if its being imported into another file, skip this part.”
if __name__=="__main__":
unittest.main()
But what about the line unittest.main()
? That line is the part that
actually runs the tests. The main()
function is part of the unittest
module. What it does is look for any and all classes that inherit from
unittest.TestCase
that it can find, and runs every single test case
from every single one of them. The code that prints out how many tests
pass and fail is part of that unittest.main()
function.