Background
For Readsr, I need to track events that recur on a particular day of the week (e.g., first Sunday of the month, third Friday of the month). I created a DayOfWeek model to store any particular event's day of the week. It contains a method next_day_of_week() to return a datetime.date object set to the next occurrence of whatever weekday a given event instance is set to (this helps with figuring out when the next occurrence of an event is).
It's easier to show through an example. On Sunday 7/3/2011:
- For an object with DayOfWeek set to Sunday, next_day_of_week() would return 7/3/2011 (current day).
- For DayOfWeek set to Monday, it would return 7/4/2011 (first subsequent Monday).
- For DayOfWeek set to Saturday, it would return 7/9/2011 (first subsequent Saturday).
Sounds simple enough. It seemed like this would be a good place to do my first unit tests.
Unit Testing
To do unit testing, the typical method is to first write test cases and then write code. In this case, I'd already written my code, so I went back and wrote test cases, trying to forget how my code worked.
To write test cases, you have to detail requirements for each method you want to test: input and expected (correct) output. The list of examples for next_day_of_week() I wrote above works for this purpose. But there's a catch: next_day_of_week() calculates the next day of the week relative to the current date, by calling datetime.date.today(). So if I write expected output for 7/3/2011, it will no longer be the correct output on 7/4/2011 or any following day. I needed a way to make datetime.date.today() always spit out my input date when I run tests, yet still continue to function normally outside of testing. Enter mocking.
Mocking
The solution was to mock out the method--to replace the real datetime.date.today() with a fake one that produces the same output no matter what day it is. To accomplish this, I used the powerful Mock library. Specifically, I needed to use the patch decorator. This decorator makes it really easy to replace on particular object within the scope of a particular method.
Before I could patch the today() method, I needed to create my own fake method. It would look like this:
1 | def faketoday() |
There's a problem, though, when I try to patch (or mock out) the method:
1 | import mock |
datetime.date
is considered a Python built-in and can't be modified.
Modifying a Class That Can't Be Modified
The trick is to write a child class that can be modified, and thus faked:
1 | class FakeDate(date): |
All this does is create a class whose constructor returns an instance of its parent's class, date. Usually, this would be pointless, but it's useful here because the new class isn't a built-in and thus can be mocked.
To use it, we simply decorate any test method that calls datetime.date.today() with a patch to replace datetime.date with FakeDate, and we also provide FakeDate a fake today() method that returns only and always the particular we are going to use for testing:
1 | class TestDayOfWeek(TestCase): |
A couple things to note: the patch only applies within this particular method, so each method to use a patch must be decorated. Also, the real datetime.date is imported in the method so we can use it inside the fake today() method. We could put this fake today() method inside FakeClass, but making it a lambda (anonymous) method assigned inside the test case gives us the flexibility to set a particular date for each test case.
Namespacing
You may be wondering why the patch decorator takes "series.models.date" as the method to replace instead of "datetime.date". That was how I tried it at first, and I was confused when it didn't work. It seemed as if the patch hadn't taken effect.
Well, it hadn't. That's because within the module being tested (models.py in the series app, or series.models in Python dotted notation), date has been imported like so:
1 | from datetime import date |
This means that within series.models, date is now available as series.models.date, so that's the name that needs to be mocked out. For more on namespacing when mocking, checked out Mock's Where to patch documentation.
Now we can supply out unit tests with any date we want, ensuring that we know what the results should be and can test against them.
References
Learning how to do this stuff, I posted my first question at Stackoverflow (I ended up answering it myself). I also learned about using a fake class from this question. Finally, the Mock documentation was very helpful as well.