Blog

The Digital Agency for International Development

Temporary monkey patches in Python tests

By Chris Wilson on 30 January 2013

Did some refactoring on the monkeypatch library to enable temporary patches using Python's context objects.

This is particularly nice for use in unit tests, where you might need to mock or stub out a component's dependencies, but you don't want those changes to be visible after your test finishes, or you only want them active for part of the test.

    def after_method_x_do_y(self, *args, **kwargs):
        y()

    from intranet_binder.monkeypatch import modify_return_value

    with modify_return_value(MyClass, 'x', after_method_x_do_y):
        MyClass().x()

I used this to make a test decorator which causes Django views that return a HttpResponseForbidden responses (Error 403) to throw an exception instead. This gives you the full stack trace where the forbidden response was thrown.

Unlike internal server errors (Error 500) these responses do not contain a stack trace, even in DEBUG mode, so this is useful for tracking them down. In this case, I wanted to know why my view was throwing a CSRF exception even though it was decorated with @csrf_exempt.

You can apply this to your tests by decorating them thus:

from intranet_binder.test_utils import throwing_exception_on_HttpResponseForbidden
@throwing_exception_on_HttpResponseForbidden
def test_login_without_csrf_is_allowed(self):
    # do something that might throw a CSRF error

Django normally disables CSRF checking in tests, to make it easier to write them, so I needed to construct a special test client, like this:

    from intranet_binder.test_utils import SuperClient
    csrf_client = SuperClient(enforce_csrf_checks=True)

    # Try a login POST without a CSRF cookie
    response = csrf_client.post(reverse('login'),
        {
            'username': self.home_user.username,
            'password': self.test_password
        }, follow=True, auto_parse_response_as_xhtml=False)

    # Did it work?
    from views import HomeView
    self.assert_followed_redirect(response, reverse(HomeView.plain_view))