Unittest in Python

0
133
11 min read

So let’s get on with it!

Basic unittest

Before we start talking about new concepts and features, let’s take a look at how to use unittest to express the ideas that we’ve already learned about. That way, we’ll have something solid to ground our new understanding into.

Time for action – testing PID with unittest

We’ll visit the PID class (or at least the tests for the PID class). We’ll write the tests so that they operate within the unittest framework.

We’ll be implementing the tests using the unittest framework.

  1. Create a new file called test_pid.py in the same directory as pid.py. Notice that this is a .py file: unittest tests are pure python source code, rather than being plain text with source code embedded in it. That means the tests will be less useful from a documentary point of view, but grants other benefits in exchange.
  2. Insert the following code into your newly-created test_pid.py (and please note that a few lines are long enough to get wrapped on the article’s page):
    from unittest import TestCase, main
    from mocker import Mocker
    import pid
    class test_pid_constructor(TestCase):
    def test_without_when(self):
    mocker = Mocker()
    mock_time = mocker.replace('time.time')
    mock_time()
    mocker.result(1.0)
    mocker.replay()
    controller = pid.PID(P=0.5, I=0.5, D=0.5,
    setpoint=0, initial=12)
    mocker.restore()
    mocker.verify()
    self.assertEqual(controller.gains, (0.5, 0.5, 0.5))
    self.assertAlmostEqual(controller.setpoint[0], 0.0)
    self.assertEqual(len(controller.setpoint), 1)
    self.assertAlmostEqual(controller.previous_time, 1.0)
    self.assertAlmostEqual(controller.previous_error, -12.0)
    self.assertAlmostEqual(controller.integrated_error, 0)
    def test_with_when(self):
    controller = pid.PID(P=0.5, I=0.5, D=0.5,
    setpoint=1, initial=12,
    when=43)
    self.assertEqual(controller.gains, (0.5, 0.5, 0.5))
    self.assertAlmostEqual(controller.setpoint[0], 1.0)
    self.assertEqual(len(controller.setpoint), 1)
    self.assertAlmostEqual(controller.previous_time, 43.0)
    self.assertAlmostEqual(controller.previous_error, -11.0)
    self.assertAlmostEqual(controller.integrated_error, 0)
    class test_calculate_response(TestCase):
    def test_without_when(self):
    mocker = Mocker()
    mock_time = mocker.replace('time.time')
    mock_time()
    mocker.result(1.0)
    mock_time()
    mocker.result(2.0)
    mock_time()
    mocker.result(3.0)
    mock_time()
    mocker.result(4.0)
    mock_time()
    mocker.result(5.0)
    mocker.replay()
    controller = pid.PID(P=0.5, I=0.5, D=0.5,
    setpoint=0, initial=12)
    self.assertEqual(controller.calculate_response(6), -3)
    self.assertEqual(controller.calculate_response(3), -4.5)
    self.assertEqual(controller.calculate_response(-1.5), -0.75)
    self.assertEqual(controller.calculate_response(‑2.25),
    ‑1.125)
    mocker.restore()
    mocker.verify()
    def test_with_when(self):
    controller = pid.PID(P=0.5, I=0.5, D=0.5,
    setpoint=0, initial=12,
    when=1)
    self.assertEqual(controller.calculate_response(6, 2), -3)
    self.assertEqual(controller.calculate_response(3, 3), -4.5)
    self.assertEqual(controller.calculate_response(‑1.5, 4),
    ‑0.75)
    self.assertEqual(controller.calculate_response(‑2.25, 5),
    ‑1.125)
    if __name__ == '__main__':
    main()
  3. Run the tests by typing:
    $ python test_pid.py

What just happened?

Let’s go through the code section and see what each part does. After that, we’ll talk about what it all means when put together.

from unittest import TestCase, main
from mocker import Mocker
import pid
class test_pid_constructor(TestCase):
def test_without_when(self):
mocker = Mocker()
mock_time = mocker.replace('time.time')
mock_time()
mocker.result(1.0)
mocker.replay()
controller = pid.PID(P=0.5, I=0.5, D=0.5,
setpoint=0, initial=12)
mocker.restore()
mocker.verify()
self.assertEqual(controller.gains, (0.5, 0.5, 0.5))
self.assertAlmostEqual(controller.setpoint[0], 0.0)
self.assertEqual(len(controller.setpoint), 1)
self.assertAlmostEqual(controller.previous_time, 1.0)
self.assertAlmostEqual(controller.previous_error, -12.0)
self.assertAlmostEqual(controller.integrated_error, 0)

After a little bit of setup code, we have a test that the PID controller works correctly when not given a when parameter. Mocker is used to replace time.time with a mock that always returns a predictable value, and then we use several assertions to confirm that the attributes of the controller have been initialized to the expected values.

def test_with_when(self):
controller = pid.PID(P=0.5, I=0.5, D=0.5,
setpoint=1, initial=12,
when=43)
self.assertEqual(controller.gains, (0.5, 0.5, 0.5))
self.assertAlmostEqual(controller.setpoint[0], 1.0)
self.assertEqual(len(controller.setpoint), 1)
self.assertAlmostEqual(controller.previous_time, 43.0)
self.assertAlmostEqual(controller.previous_error, -11.0)
self.assertAlmostEqual(controller.integrated_error, 0)

This test confirms that the PID constructor works correctly when the when parameter is supplied. Unlike the previous test, there’s no need to use Mocker, because the outcome of the test is not supposed to be dependant on anything except the parameter values—the current time is irrelevant.

class test_calculate_response(TestCase):
def test_without_when(self):
mocker = Mocker()
mock_time = mocker.replace('time.time')
mock_time()
mocker.result(1.0)
mock_time()
mocker.result(2.0)
mock_time()
mocker.result(3.0)
mock_time()
mocker.result(4.0)
mock_time()
mocker.result(5.0)
mocker.replay()
controller = pid.PID(P=0.5, I=0.5, D=0.5,
setpoint=0, initial=12)
self.assertEqual(controller.calculate_response(6), -3)
self.assertEqual(controller.calculate_response(3), -4.5)
self.assertEqual(controller.calculate_response(-1.5), -0.75)
sel+f.assertEqual(controller.calculate_response(‑2.25),
‑1.125)
mocker.restore()
mocker.verify()

The tests in this class describe the intended behavior of the calculate_response method. This first test checks the behavior when the optional when parameter is not supplied, and mocks time.time to make that behavior predictable.

def test_with_when(self):
controller = pid.PID(P=0.5, I=0.5, D=0.5,
setpoint=0, initial=12,
when=1)
self.assertEqual(controller.calculate_response(6, 2), -3)
self.assertEqual(controller.calculate_response(3, 3), -4.5)
self.assertEqual(controller.calculate_response(‑1.5, 4),
‑0.75)
self.assertEqual(controller.calculate_response(‑2.25, 5),
‑1.125)

In this test, the when parameter is supplied, so there is no need to mock time.time. We just have to check that the result is what we expected.

The actual tests that we performed are the same ones that were written in the doctest. So far, all that we see is a different way of expressing them.

The first thing to notice is that the test file is divided up into classes that inherit from unittest.TestCase, each of which contains one or more test methods. The name of each test method begins with the word test, which is how unittest recognizes that they are tests.

Each test method embodies a single test of a single unit. This gives us a convenient way to structure our tests, grouping together related tests into the same class, so that they’re easier to find.

Putting each test into its own method means that each test executes in an isolated namespace, which makes it somewhat easier to keep unittest‑style tests from interfering with each other, relative to doctest‑style tests. It also means that unittest knows how many unit tests are in your test file, instead of simply knowing how many expressions there are (you may have noticed that doctest counts each >>> line as a separate test). Finally, putting each test in its own method means that each test has a name, which can be a valuable feature.

Tests in unittest don’t directly care about anything that isn’t part of a call to one of the assert methods of TestCase. That means that when we’re using Mocker, we don’t have to be bothered about the mock objects that get returned from demonstration expressions, unless we want to use them. It also means that we need to remember to write an assert describing every aspect of the test that we want to have checked. We’ll go over the various assertion methods of TestCase shortly.

Tests aren’t of much use, if you can’t execute them. For the moment, the way we’ll be doing that is by calling unittest.main when our test file is executed as a program by the Python interpreter. That’s about the simplest way to run unittest code, but it’s cumbersome when you have lots of tests spread across lots of files.

if __name__ == ‘__main__’: might look strange to you, but its meaning is fairly straight forward. When Python loads any module, it stores that module’s name in a variable called __name__ within the module (unless the module is the one passed to the interpreter on the command line). That module always gets the string ‘__main__’ bound to its __name__ variable. So, if __name__ == ‘__main__’: means—if this module was executed directly from the command line.

Assertions

Assertions are the mechanism that we use to tell unittest what the important outcomes of the test are. By using appropriate assertions, we can tell unittest exactly what to expect from each test.

assertTrue

When we call self.assertTrue(expression), we’re telling unittest that the expression must be true in order for the test to be a success.

This is a very flexible assertion, since you can check for nearly anything by writing the appropriate boolean expression. It’s also one of the last assertions you should consider using, because it doesn’t tell unittest anything about the kind of comparison you’re making, which means that unittest can’t tell you as clearly what’s gone wrong if the test fails.

For an example of this, consider the following test code which contains two tests that are guaranteed to fail:

from unittest import TestCase, main
class two_failing_tests(TestCase):
def test_assertTrue(self):
self.assertTrue(1 == 1 + 1)
def test_assertEqual(self):
self.assertEqual(1, 1 + 1)
if __name__ == '__main__':
main()

It might seem like the two tests are interchangeable, since both test the same thing. Certainly they’ll both fail (or in the unlikely event that one equals two, they’ll both pass), so why prefer one over the other?

Take a look at what happens when we run the tests (and also notice that the tests were not executed in the same order as they were written; tests are totally independent of each other, so that’s okay, right?):

Do you see the difference? The assertTrue test was able to correctly determine that the test should fail, but it didn’t know enough to report any useful information about why it failed. The assertEqual test, on the other hand, knew first of all that it was checking that two expressions were equal, and second it knew how to present the results, so that they would be most useful: by evaluating each of the expressions that it was comparing and placing a != symbol between the results. It tells us both what expectation failed, and what the relevant expressions evaluate to.

assertFalse

The assertFalse method will succeed when the assertTrue method would fail, and vice versa. It has the same limits in terms of producing useful output that assertTrue has, and the same flexibility in terms of being able to test nearly any condition.

assertEqual

As mentioned in the assertTrue discussion, the assertEqual assertion checks that its two parameters are in fact equal, and reports a failure if they are not, along with the actual values of the parameters.

assertNotEqual

The assertNotEqual assertion fails whenever the assertEqual assertion would have succeeded, and vice versa. When it reports a failure, its output indicates that the values of the two expressions are equal, and provides you with those values.

assertAlmostEqual

As we’ve seen before, comparing floating point numbers can be troublesome. In particular, checking that two floating point numbers are equal is problematic, because things that you might expect to be equal—things that, mathematically, are equal—may still end up differing down among the least significant bits. Floating point numbers only compare equal when every bit is the same.

To address that problem, unittest provides assertAlmostEqual, which checks that two floating point values are almost the same; a small amount of difference between them is tolerated.

Lets look at this problem in action. If you take the square root of 7, and then square it, the result should be 7. Here’s a pair of tests that check that fact:

from unittest import TestCase, main
class floating_point_problems(TestCase):
def test_assertEqual(self):
self.assertEqual((7.0 ** 0.5) ** 2.0, 7.0)
def test_assertAlmostEqual(self):
self.assertAlmostEqual((7.0 ** 0.5) ** 2.0, 7.0)
if __name__ == '__main__':
main()

The test_assertEqual method checks that Python Testing: Beginner's Guide, which is true in reality. In the more specialized number system available to computers, though, taking the square root of 7 and then squaring it doesn’t quite get us back to 7, so this test will fail. More on that in a moment.

Test test_assertAlmostEqual method checks that Python Testing: Beginner's Guide, which even the computer will agree is true, so this test should pass.

Running those tests produces the following, although the specific number that you get back instead of 7 may vary depending on the details of the computer the tests are being run on:

Unfortunately, floating point numbers are not precise, because the majority of numbers on the real number line can not be represented with a finite, non-repeating sequence of digits, much less a mere 64 bits. Consequently, what you get back from evaluating the mathematical expression is not quite 7. It’s close enough for government work though—or practically any other sort of work as well—so we don’t want our test to quibble over that tiny difference. Because of that, we should use assertAlmostEqual and assertNotAlmostEqual when we’re comparing floating point numbers for equality.

This problem doesn’t generally carry over into other comparison operators. Checking that one floating point number is less than the other, for example, is very unlikely to produce the wrong result due to insignificant errors. It’s only in cases of equality that this problem bites us.

LEAVE A REPLY

Please enter your comment!
Please enter your name here