Quick Introduction to Mock
Recently updated on
Mock is a powerful library for facilitating testing. It allows users to replace components of your application with powerful mock objects at testing runtime. These objects can then be used to make observations about call patterns and to assert which methods of the object were accessed, the number of times a method is called, and with what parameters the method was called. Additionally, you can specify return values and raise exceptions from these mock calls to test your application under certain situations. Mock objects can also be used to replace entire objects with lightweight objects to alleviate maintaining complex states for simple comparisons. The following examples are for using mock in a Python 2.7 project. Mock is included in Python’s standard library as of Python 3.3 and is available as unittest.mock. A gist of everything in this post is available here:https://gist.github.com/danrjohnson/98b2220a4c694dc51f88
Throughout this post, we will use the following class and inspect how we can use mock to create powerful and meaningful tests without unnecessary calls to third party resources or external services.
# person.py from datetime import date from django.core.mail import send_mail class Person(object): def __init__(self, given_name, family_name, email, dob, *args, **kwargs): self.given_name = given_name self.family_name = family_name self.email = email self.dob = dob self.args = args self.kwargs = kwargs def get_full_name(self): """ Display full name of Person """ return '%s %s' % (self.given_name, self.family_name) def get_age(self): """ Returns Person's age in years as an integer """ today = date.today() return today.year - self.dob.year - ( (today.month, today.day) < (self.dob.month, self.dob.day) ) def send_birthday_email(self): """ Sends Person a congratulatory email if today is his or her birthday """ today = date.today() if (today.month, today.day) == (self.dob.month, self.dob.day): send_mail( 'Happy Birthday, %s!' % self.given_name, 'Now that its your birthday....', 'email@example.com', [self.email] ) def is_family_member(self, person_two): """ Naively determine if two Persons are family by comparing family_name """ if self.family_name == person_two.family_name: return True return False
Mock objects can be used inside of existing tests to quickly create objects that might otherwise take more construction effort than necessary for a given test.
Let’s write a test for the get_age method. We need a way to predictably determine a person’s age with the understanding that every day, date.today() will return a different value. Luckily, mock provides a patch decorator which allows us to override the date module of the datetime package. Let’s look at how we can do that.
@mock.patch('person.date') def test_get_age(self, datetime_mock): datetime_mock.today.return_value = date(2013, 10, 19) self.assertEqual( self.person.get_age(), 29 )
We override the date module and now have an additional parameter to our test function date_mock. What this new parameter allows us to do is set a return_value for the date.today method. We then set that return value to a set date. Since we now know the value that date.today() will return, we can accurately predict that our Person’s age will be 29. If we had wanted to raise an exception for some reason, instead of using return_value we could have used side_effect and specified an exception as that value. Note that we needed to override person.date not datetime.date as might be expected. This is patching the imported date type for the person module. Getting used to this pattern of what to override can take some time.
Next we are going to write a couple of tests for send_birthday_email. We need to test that when it is a Person’s birthday we send them a birthday email and when it’s not a Person’s birthday then we do not send them an email. We don’t actually want to send an email though since we trust that Django’s internals will take care of that and we trust that Django’s tests of send_mail are sufficient. We only care that the call to send_mail happened with a specified set of arguments. To do this, we will create a patch for send_mail and check to see with what it was called. Similarly, if it isn’t the user’s birthday, we want to verify that send_mail isn’t called at all. Lets take a look at the test for when it is the Person’s birthday.
mock.patch('person.date') mock.patch('person.send_mail') def test_send_birthday_email_is_birthday(self, send_mail_mock, date_mock): date_mock.today.return_value = date(2014, 7, 4) self.person.send_birthday_email() send_mail_mock.assert_called_once_with( 'Happy Birthday, Dan!', 'Now that its your birthday....', 'firstname.lastname@example.org', ['email@example.com'] )
Here we see that we called a method on send_mail_mock called assert_called_once_with. This asserts that send_mail_mock was only called one time and that its call arguments are the arguments we passed into the function. Mock objects have several methods similar to assert_called_once_with. The method assert_called_with asserts that the last call to the function or method was with the specified arguments. The method assert_any_call asserts that the function or method has ever been called with the specified arguments.
The last test we need to write is to verify no email is sent if it isn’t the Person’s birthday. To do this, we just need to confirm that send_mail was not called. Here is the test.
mock.patch('person.date') mock.patch('person.send_mail') def test_send_birthday_email_not_birthday(self, send_mail_mock, date_mock): date_mock.today.return_value = date(2014, 1, 1) self.person.send_birthday_email() self.assertFalse(send_mail_mock.called)
Mock objects have an boolean attribute name called that tells us if mock object was called at any point. All we need to do is assert that called is False.
We can also use mock to quickly create objects for testing. To do this, we just need to instantiate a Mock object. Then we can set attributes and assign functions to that mock object accordingly. Our Person class has the method is_family_member which compares the family_name of the Person with the family_name of another Person object. Obviously this is a terrible way to determine this, but for example’s sake this should suffice. We don’t necessary want to create a new Person object as we only really care about the family_name attribute and changes to the attributes on our class might make maintaining this second object troublesome when we are only interested in a single attribute of that object. Here are the tests:
def test_is_family_member_not_same_family_name(self): person_two = mock.Mock() person_two.family_name = 'Davenport' self.assertFalse(self.person.is_family_member(person_two)) def test_is_family_member_same_family_name(self): person_two = mock.Mock() person_two.family_name = 'Johnson' self.assertTrue(self.person.is_family_member(person_two))
Here we instantiate a Mock object and set a family name attribute. This allowed us to emulate another Person object without having to keep track of its complete state down the road. This might have some unintended consequences down the road as undefined attributes on the Mock object will return other Mock objects. So for instance if we compared the first_name of each of these objects, one would be “Dan” and the other would be another Mock object.