I lied in my last post. I have decided to do one more ELI5, this time for Unit Tests. I decided to do this to help my classmates – Google is running a code retreat tomorrow at my university and advised participants to be prepared to work with unit tests but many of my classmates haven’t worked with them before. I figured I’d help them out by giving a very quick introduction to the package and giving a basic template.
Quick Introduction
Unit testing is an approach to software testing where you test specific parts, or units, of your code. For example, if I wanted to write software for computing the area of various shapes I could have unit tests that refer to sample shapes and the areas it should output for these shapes. It is quite common in test driven development, where you write a program by first writing the tests it will need to pass. If you’ve ever attended a coderdojo chances are you are very familiar with the test driven development paradigm.
One of the big benefits of this type of approach is can make your code much easier to maintain. I can make a change to my code and immediately run a suite of tests that will tell me exactly what it broke. Then I can determine whether my change was an error or where I have to make additional changes to ensure that everything else works correctly.
Using the unittest package
The package I am going to use for this is unittest, which is a pretty basic package for this sort of thing. There can be other packages that output more readable results, or integrate with the specific IDE you are using, so once you are used to the concept feel free to move on to whatever you prefer. Also, this package uses object orientated programming, so check out my Objects and ubclassing tutorials to learn about these concepts.
To start with, here is a basic template.
import unittest # any other import statement class TestName(unittest.TestCase): def test_SAMPLE_METHOD(self): # Arrange Put "arrange" code here # Actual Put "actual" code here # Assert Put "assert" code here if __name__ == '__main__': unittest.main()
So, in the first line I simple import the unittest package. I believe it comes with python, if not please feel free to correct me. After that you do whatever other imports you need and then you create your tests as methods within a subclass of the TestCase object from the unittest package. Don’t know what some of those words mean? Check out my ELI5s linked above. (Hmm, maybe I should have called this an ELI6?). For your test methods, they must all begin with the word “test” and ideally the rest should be what you are testing written in the form of (input in method is output). You’ll also see that I split my code into three parts, I’ll explain more what these are below.
At the end you will might be seeing something new in the last two lines. This code is simply stating to run the unittest.main() method when the file is run in python, which will go to go through all your test methods and tell you what the outcomes are.
For this example, I will build a unittest to check if the vrooom method in my car works properly. To remind you, here is the car code:
class Car: mileage = 0 def vrooom(self, distance): self.mileage += distance
If you want to follow along, save the car code to a file named my_car_code.py and save the test template into a file called my_car_tests.py. Make sure both are in the same directory.
Arrange
The first step in your unit test is the arrange step. Here you arrange everything so it is ready for the test. This happens in two parts, first by making sure you are importing everything and then by creating an instance of the object you want to test and manipulating it if needed. Here is an example of the arrange step.
import unittest from my_car_code import Car class TestCar(unittest.TestCase): def test_10_in_vrooom_distance_updates_mileage_to_10(self): # Arrange test_car = Car() expected = 10 # Actual Put "actual" code here # Assert Put "assert" code here if __name__ == '__main__': unittest.main()
Here I changed my import statement to import the Car object from my_car_code.py. Note that this will only work if both the my_car_code.py and my_car_tests.py are in the same directory. Then, I changed the name of the TestCase subclass and the test method to better explain what I am testing. Then I created an instance of my Car object and I entered an expected value value that the result of my test should be.
Actual
The actual step is one of the simplest steps. You basically just run the code to get the actual value it outputs. This is usually pretty simple – just a single function call.
import unittest from my_car_code import Car class TestCar(unittest.TestCase): def test_10_in_vrooom_distance_updates_mileage_to_10(self): # Arrange test_car = Car() expected = 10 # Actual test_car.vrooom(10) actual = test_car.mileage # Assert Put "assert" code here if __name__ == '__main__': unittest.main()
Since I am testing what happens when the vroooom method takes a value of 10, I simply write test_car.vrooom(10). Then, since this method updates the mileage attribute, I set a variable called actual to be equal to the new mileage.
Assert
The final step is the assert step. At this step you compare the actual and the expected to see if your code works. In this case it is pretty simple – we check if the expected and actual values are equal.
import unittest from my_car_code import Car class TestCar(unittest.TestCase): def test_10_in_vrooom_distance_updates_mileage_to_10(self): # Arrange test_car = Car() expected = 10 # Actual test_car.vrooom(10) actual = test_car.mileage # Assert self.assertEqual(actual, expected) if __name__ == '__main__': unittest.main()
I don’t use normally boolean syntax to check if it’s qual, but a specific method called assertEqual. There are many other methods that can be used in other situations. When writing your own tests use whichever ones are most appropriate.
Run the test
Now, we are ready to test our code. Simple make sure that both files are in the same directory and run the my_car_tests.py. If successful you will get a message like below:
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
This is telling you it ran 1 test and it was OK. Yay!
Let’s see what happens if something went wrong though! First, if the code has a mistake (for example it doesn’t actually update the mileage) you might get something like this:
F ====================================================================== FAIL: test_10_in_vrooom_distance_updates_mileage_to_ten (__main__.TestCar) ---------------------------------------------------------------------- Traceback (most recent call last): File "my_car_tests.py", line 13, in test_10_in_vrooom_distance_updates_mileage_to_ten self.assertEqual(actual, expected) AssertionError: 0 != 10 ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
This is telling you that the test_10_in_vrooom_distance_updates_mileage_to_ten method failed because actual was 0 which is not equal to 10.
You’ll also be notified if the code generates an error. In the example below, I am entering the string ’10’ into the vrooom method instead of the int 10. This generates a TypeError as it can’t add a string to an int.
E ====================================================================== ERROR: test_10_in_vrooom_distance_updates_mileage_to_10 (__main__.TestCar) ---------------------------------------------------------------------- Traceback (most recent call last): File "my_car_tests.py", line 10, in test_10_in_vrooom_distance_updates_mileage_to_10 test_car.vrooom('10') File "/home/roy/personal/my_car_code.py", line 4, in vrooom self.mileage += distance TypeError: unsupported operand type(s) for +=: 'int' and 'str' ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (errors=1)
Note that some IDEs incorporate pretty formatting for unittest’s outputs, which can make finding the broken test and determine the cause much easier, especially with a very large number of tests.
Conclusion
So there you have it, a basic introduction to using the unittest package in python. If you want to practice more, add in a method to test that mileage updates to 20 if it was already set at 10.