In this post I present a tutorial on how I unit test python code and some good examples of the power of the mock library.
On a my recent visit to Amsterdam, I popped into the EU office and spent a day teaching unit testing and proper mocking techniques. It was fun to share the wisdom, however, the devs had few resources available to them and didn't quite understand the nuances of more advanced unit testing techniques. Since back, I have found myself teaching the techniques to a friend. I figured it was appropriate to put together a blog post sharing what I know with others.
Throughout this post, I am putting all of my code in a single file called examples.py. Normally, you would have a separate directory for test code, but for ease, I am using a single file. To run tests via the command line, be sure to be in the same directory as your example.py and run:
nosetests -vs --tests=examples.py
Here is a very simple python method:
[source="python"] def square(n): """ A simple method to take in a number n and return the square of the number """ if not isinstance(n, (int, float)): raise RuntimeError('Argument must be an int or float.') return n * n [/source]
A simple set of Tests:
[source="python"] class SquareTestCase(unittest.TestCase): """ A set of tests surrounding our square method """ def test_positive_int(self): self.assertEqual(square(2), 4) def test_positive_float(self): self.assertEqual(square(.5), .25) def test_negative_int(self): self.assertEqual(square(-9), 81) def test_errors(self): self.assertRaises(RuntimeError, square, 'NaN') [/source]
If you run nose tests now, you should get something like:
$ nosetests -vs --tests=examples.py test_negative_int (examples.SquareTestCase) ... ok test_none_int (examples.SquareTestCase) ... ok test_positive_float (examples.SquareTestCase) ... ok test_positive_int (examples.SquareTestCase) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
Important Best Practices to Note:
Even though our square method is only a few lines, we have 4 tests. This may be overkill for this simple of method, but it is important to note what is going on.
The python mock library contains 2 main components
mock.Mock and mock.MagicMock
Multipurpose classes that can emulate objects, methods, etc. Properties, methods, etc can all be overridden. These classes maintains a listing of if the "thing" mocked was called, how many times it was called, and with what arguments. You can also have the "thing" throw Exceptions and other side effects. The key difference between Mock and Magic mock is that the later will not throw an AttributeError if a property does not exist on it. Rather it will simply return None.
A method that takes in string path to a module, class, method, etc and replaces it with an instance of the above Mock class. This is good for when your code calls other code that:
An Example where Mocking is Useful
Consider the following code that generates a number representing the season based on todays date.
[source="python"] import datetime as dt # Put this at the top of the file... def get_season(): """ Method to calculate the season of the current day (1=Spring, 2=Summer, 3=Fall, 4=Winter) """ d = dt.date.today() month = d.month if month in [3,4,5]: return 1 if month in [6,7,8]: return 2 if month in [9,10, 11]: return 3 if month in [12, 1, 2]: return 4 else: raise RuntimeError('Invalid value for month %s' % month) [/source]
Lets think about how you would test this without mocking for a second. Since the method uses today's date, the value of the method depends on when the test is run. So for example, if you ran the test in January, you would get 1. Running it 3 months later would give you a different value. This leads itself to unpredictability. You could test that get_season returns something. You could test that it is an integer. You could test that it is also between 1 and 12 inclusive. However, we have four logic branches here each with 3 conditions (i.e. the set of month numbers). That said, without mocking, we cannot test the specific cases nor do we have a ton of confidence in the predictability of the test.
Mock the datetime module to control what "today" is
To more thoroughly test our method, we want to control what the value of "today" is. We will want to create a fake python date object and then patch the datetime python module so we can control what it returns. Note: We patch the import of the datetime module not the module itself. Many low level python modules, such as datetime, cannot be patched and thus the import of these modules needs to be what is patched.
[source="python"] class GetSeasonTestCases(unittest.TestCase): """ Tests Surrounding get_season method """ @patch('examples.dt') def test_base(self, mocked_datetime): # Setup Mocking of datetime.date.today and create mocked date object mocked_today = Mock() mocked_datetime.date.today.return_value = mocked_today # Run our tests mocked_today.month = 1 self.assertEqual(get_season(), 4) mocked_today.month = 2 self.assertEqual(get_season(), 4) mocked_today.month = 3 self.assertEqual(get_season(), 1) mocked_today.month = 4 self.assertEqual(get_season(), 1) mocked_today.month = 5 self.assertEqual(get_season(), 1) mocked_today.month = 6 self.assertEqual(get_season(), 2) mocked_today.month = 7 self.assertEqual(get_season(), 2) mocked_today.month = 8 self.assertEqual(get_season(), 2) mocked_today.month = 9 self.assertEqual(get_season(), 3) mocked_today.month = 10 self.assertEqual(get_season(), 3) mocked_today.month = 11 self.assertEqual(get_season(), 3) mocked_today.month = 12 self.assertEqual(get_season(), 4) # Check to ensure that we called our mocked .today() method self.assertEqual(mocked_datetime.date.today.call_count, 12) [/source]
As you can see, through the use of mocking we were able to test how our code handles regardless of when the test was run. Some key things to note:
At this point, it is important to note that we have treated the python datetime libary as a black box of sorts. We make calls to it and we expect things back, but we don't actually care what it is doing under the hood. For all we know, the python datetime libary makes all sorts of api calls to the hardware that are cpu expensive, dependent on OS, or invoke some sort of black magic. We also assume that the developers of the library have written their own unit tests for it and as such, there is no need to actually test their code. As such, using mocks, we treat it like a black box and assume it will work. This might seem silly for something as simple as datetime, but it is the mindset you should get in when testing your code.
Next up, we actually peer into the black box a bit... (2nd post coming soon)