Monday, March 30, 2015

Testing Django: Setting up a patch for an entire TestCase (or how to configure a mock object for an entire TestCase)

Often times you'll want to mock out some functionality in your tests. For example, if you're testing a Twitter API implementation, you don't want to actually hit the Twitter API when running your tests. You just want to "fake" a Twitter API response to make sure your code works properly. If you've used Mock objects before, you'll know that they provide the superpowers to enable this "faking" voodoo I speak of.

It's typical to mock out some functionality in your Django tests using the patch decorator:
class LoginViewTest(TestCase):
 def setUp(self):
  # set some stuff up

 @patch('django_twitter_auth.views.redirect')
 def test_view_redirects_to_auth_url(self, mock_redirect):
  # do some tests
In the above example, we're replacing the redirect function with a Mock (technically a MagicMock) object. So whenever the redirect function is called within the context of the test_view_redirects_to_auth_url test, it'll actually be calling the Mock object which we have complete control over.

If you want to mock out an object for a bunch of tests, you could apply the patch decorator to each test. You could also apply the patch decorator to the class itself:
@patch('django_twitter_auth.views.redirect')
class LoginViewTest(TestCase):
 def setUp(self):
  # set some stuff up

 def test_view_redirects_to_auth_url(self, mock_redirect):
  # do some tests

 def some_other_test(self, mock_redirect):
  # do some tests
But if each test, for example, requires the mock_redirect to be setup and configured to behave a specific way, you'll easily end up repeating a lot of code in each test.

Unfortunately you can't just pass in the mocked object to your setUp method like this:
@patch('django_twitter_auth.views.redirect')
class LoginViewTest(TestCase):
 def setUp(self, mock_redirect):
  # set the mock redirect for all the tests

 ...
But wait! There is an alternative solution that allows you to configure your Mock objects in your test class' setUp method:
class LoginViewTest(TestCase):
 def setUp(self):
  # provide the location of the object you want to mock
  patcher = patch('django_twitter_auth.views.Twython')
  # explicitly start the patch 
  self.mock_Twython = patcher.start()
  # make sure the patch is removed
  self.addCleanup(patcher.stop)

  # mock out all functionality required for view
  self.mock_Twython.some_attribute = "foo"
  self.mock_Twython.some_method.return_value = "bar"

 def test_Twython_instance_initialized_with_app_tokens(self):
  # the test has access to the Mock object!
  # let's do some fake setup here that's unique to
  # this test function
  mock_Twython_instance = self.mock_Twython.return_value
  
  # do some tests ...

  # assert some stuff

 def some_other_Twython_test(self):
  # this test also has access to the Mock object
  # watch him do some unique setup, like overriding
  # the default setup maybe?
  self.mock_Twython.some_method.return_value = "uniquebar"

  # do some tests ...

  # assert some stuff
Take away the decorator magic, and patch can be used like a normal function. It returns a patcher object. When you call the patcher object's start() method, it returns the mocked out object. The only caveat is that you need to explicitly make sure the patching is "undone". This is what the:
    self.addCleanup(patcher.stop)  
line in the setUp method in the above example does.

You can mock out multiple objects in setUp, just assign your patcher objects to different variable names like patcher_1, patcher_2, ..., etc. and then call patcher_1.start(), patcher_2.start(), ..., etc. to return the object each patcher object is mocking out.

You can read more about the start() and stop() methods in the official Mock documentation here.

Happy testing!