The first unit testing framework, JUnit, was invented by Kent Beck and
Erich Gamma in 1997, for testing Java programs. It was so successful that
the framework has been re-implemented in every major programming language.
Here we discuss the Python version, unittest.
Unit testing is testing individual "units," or functions, of a program.
It does not have a lot to say about system integration--whether the
various parts of a program fit together. That's a separate issue.
The goals of a unit testing framework are:
F5 in IDLE). Ideally, you should be comfortable
running tests after every change in the program, however minor.Let's be honest: If it isn't easy, nobody is going to do it. Programmers are only human. The genius of the JUnit framework (and similar frameworks for just about every language) is that it does make it--well--a lot easier, anyway. Easy enough so that the next time you are too lazy to write good tests, and consequently spend hours debugging, you'll promise yourself to use TDD the next time.
For a first example, we will use the following code:
def add(x, y):
return x * y
and save it in a file named arithmetic.py.
We write our tests as a set of functions. Tests are usually put in a
separate file, and we'll do that here. We'll call the file testArithmetic.py.
Since it is a different file, it will have to import arithmetic
in order to access our add method. It will also have to import
unittest, Python's unit test framework.
Finally, we need to put our test methods in a class,
and tell unittest to run the methods in that class. Because
they are in a class, we need to use the variable self in
several places. We haven't discussed classes yet, so for the time being,
just follow the pattern shown below. Here's the complete structure:
from arithmetic import * # import everything from your module
import unittest # This loads the testing methods and a main program
class TestArithmetic(unittest.TestCase): # use any meaningful name
## Your test methods go here. Indent your
## methods, because they belong inside the class.
unittest.main() # outside the class--this tells the framework to run
Here's an actual test:
def testAdd(self):
self.assertEqual(4, add(2, 2))
and here are the results of the test:
.
----------------------------------------------------------------------
Ran 1 test in 0.009s
OK
(plus several other lines, which you can ignore)
You probably noticed that there is a mistake in the add
method. I "accidentally" picked a pair of numbers that would cause the test
to pass (there is one other pair). We can put additional tests within the
same method--in fact, it's just a Python method, so we can put arbitrary
Python code in it. For example,
def testAdd(self):
self.assertEqual(4, add(2, 2)) # first test
self.assertEqual(10, add(3, 7)) # second test
self.assertEqual(90, add(-10, 100)) # third test
With this code, the "first test" will pass, but the "second test" will
fail. The failure will halt execution of this test method (your
other test methods will still run), so the "third test" will never be used.
The result looks like:
F
======================================================================
FAIL: testAdd (__main__.TestArithmetic)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Volumes/THUMBDRIVE/PythonPrograms/arithmeticTest.py", line 15, in testAdd
self.assertEqual(10, arithmetic.add(3, 7)) # second test
AssertionError: 10 != 21
----------------------------------------------------------------------
Ran 1 test in 0.027s
FAILED (failures=1)
(plus several other lines, which you can ignore)
Notice that the test shows us:testAdd,
not in add itself),How many tests should you have?
3+7==10
above). However, if this is all you do, you are following thetest',
otherwise it will be ignored. This is so that you can write "helper"
methods you can call from your tests, but are not directly called by the
test framework.self.self.' in front of every built-in assertion
method you call.import
arithmetic. To call a method from that module, you must prefix
it with the module name, for example, arithmetic.add(2, 2).from arithmetic import *.
Then you can call the methods directly, as for instance, add(2,
2).message parameter, to be printed if the test
fails--the brackets, [ ], indicate an optional
argument that can be omitted. In most cases, the message is superfluous;
don't use it unless you really have something useful to add.
|
If you write a method setUp(self), the unittest
framework will call setUp before each and every one of your
tests.
Remember that the order in which your test methods run is not
guaranteed. If one of your tests changes the values of global
variables, those changes may or may not have been performed when some
other test is run. In order to test successfully, you need to restore all
global variables to their initial state before each test. Do this
initialization in the setUp(self) method.
Better yet, don't use global variables. There is almost always a better way to do things.
pick a method that doesn't depend on other, untested methods
while the method isn't complete:
write a test for the desired feature
run all tests and make sure the new one fails
while any test fails:
add/fix just enough code to try to pass the tests
refactor the code to make it cleaner
Except for very simple methods, you can usually write the method in stages. Try to take small steps. Write a test for the simplest case you can think of, then run it. The test should fail--if it doesn't, then either the method already handles that case, or you did something wrong in the test. This is a quick way to make sure you aren't wasting your time adding code to the method.
Next, write just enough code to make the test pass. The temptation may be strong to write more code than necessary, but resist. Don't write any code that you don't already have a test for. Make sure the new code passes, or debug it until it does.
At this point your method may not do everything it needs to, but it will be correct in the part that it does. Now is a good time to think briefly about how to refactor (make cleaner) the code. Is there a variable that could have a better name? Would it be more readable if you rearranged the parts of an if statement? Could you pull out a chunk of the code and make it a function? Did you repeat code? Refactoring is all about making the code better without changing what it does.