Chapter 8: Unit testing

Test fixtures

face Josiah Wang

For our next discussion, let’s consider testing for our racing car game. If you recall, back in Lesson 9, I added Wheels to our Car. To test our Car, we need to first create Wheels for the Car instance.

Below are some example test cases. I am testing the accelerate() and decelerate() methods of Car.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def test_accelerate():
    # create 4 wheels
    diameter = 36
    resistance = 0.5
    num_of_wheels = 4
    wheels = [Wheel(diameter, resistance) for i in range(num_of_wheels)] 
    car = Car("Honda Civic", 2015, False, wheels)

    car.accelerate()
    assert car.speed == 1 - (0.5*resistance*num_of_wheels) # 0

    car.accelerate(10)
    assert car.speed == 10 - (0.5*resistance*num_of_wheels) # 9


def test_decelerate():
    # create 4 wheels
    diameter = 36
    resistance = 0.5
    num_of_wheels = 4
    wheels = [Wheel(diameter, resistance) for i in range(num_of_wheels)]
    car = Car("Honda Civic", 2015, False, wheels)

    car.accelerate(10)
    assert car.speed == 9

    car.decelerate(4) 
    assert car.speed == 6

Obviously, this code is pretty repetitive. You have to create new Wheel instances and then a new Car instance from the wheels, and then run different tests on the same Car instance.

You could just create the Car instance once, and reuse it for both tests. But then you will have to remember to ‘reset’ the properties of the Car after each test (the speed of the car should be 0, rotation 0 etc.) Remember that each unit test should be independent of each other - we don’t want side effects from any previous tests!

You can possibly have a separate, non-test function to create the Car instance, but you will still have to call it from your test function each time.

Test fixtures

What if you can automate the process of creating a new, fresh, Car instance every time you call a test function? You might also want to clean up everything once you are done. For example, you might want to delete some temporary files you created or generate some reports.

In software testing, such setups are called test fixtures. You put all related tests into the same class. All tests will share the same ‘setting up’ process. The ‘setting up’ process will run before each test is run. Similarly, a ‘clean up’ process will happen after each test has been completed.

For our example, you can subclass from unittest.TestCase and turn the above test functions into methods. You then create your Car instance inside a setUp() method (Line 6). You can optionally also have a tearDown() method (Line 14) that runs after each test is complete. Note that the module is based on a Java library, which is why the method names follow Java’s camelCase convention instead of Python’s recommended lower_case convention.

I have also included some print() statements in each method so that you can convince yourself that the methods have been invoked! Save this file as test_car.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import unittest

from car import Car, Wheel

class CarTest(unittest.TestCase):
    def setUp(self):
        print("Setting up")
        diameter = 36
        resistance = 0.5
        num_of_wheels = 4
        self.wheels = [Wheel(diameter, resistance) for i in range(num_of_wheels)]
        self.car = Car("Honda Civic", 2015, False, self.wheels)

    def tearDown(self):
        print("Tearing down")

    def test_accelerate(self):
        print("Test accelerate")
        self.car.accelerate()
        self.assertEqual(self.car.speed, 0)
        self.car.accelerate(10)
        self.assertEqual(self.car.speed, 9)

    def test_decelerate(self):
        print("Test decelerate")
        self.car.accelerate(10)
        self.assertEqual(self.car.speed, 9)
        self.car.decelerate(4) 
        self.assertEqual(self.car.speed, 6)

You can download car.py and save it to the same directory if you want to run this piece of code. Feel free to mess with the code if you want to see failed tests!

If you run the test, you can see that setUp() is run before each test, and tearDown() is run after each test. The . is printed by the test runner to indicate a successful test (remember .. from earlier?)

user@MACHINE:~$ python3 -m unittest test_car
Setting up
Test accelerate
Tearing down
.Setting up
Test decelerate
Tearing down
.
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
user@MACHINE:~$

Hopefully this gives you a basic glimpse of what you can do with test fixtures, especially once you start writing more complex test cases.