Django’s assertRedirects little gotcha
Something we’ve be trying to pay more attention to with our newest green field development projects is the running time of our unit test suites. One of the projects was running ~200 unit tests in 2 seconds. As development continued and the test case number grew, it started taking 10 seconds, then over 30 seconds. Something wasn’t right.
First challenge was to determine which were the slow running tests. A little googling found this useful patch to the python code base. Since we are using python 2.5 and virtual environments we decided to simply monkey patch it. This makes verbosity level 2 spit out the run time for each test. We then went one step further and made the following code change to _TextTestResult.addSuccess:
def addSuccess(self, test): TestResult.addSuccess(self, test) if self.runTime > 0.1: self.stream.writeln("\nWarning: %s runs slow [%.3fs]" % (self.getDescription(test), self.runTime)) if self.showAll: self.stream.writeln("[%.3fs] ok" % (self.runTime)) elif self.dots: self.stream.write('.')
With it now easy to tell which were our slow tests we set out to make them all fast again. As expected the majority of the cases were an external service not being mocked correctly. Most of these were easily solved. But there were a few tests where we couldn’t find what hadn’t been mocked. Adding a few timing statements within these tests revealed the culprit. The Django frameworks assertRedirects method.
def assertRedirects(self, response, expected_url, status_code=302, target_status_code=200, host=None): """Asserts that a response redirected to a specific URL, and that the redirect URL can be loaded. Note that assertRedirects won't work for external links since it uses TestClient to do a request. """ if hasattr(response, 'redirect_chain'): # The request was a followed redirect self.failUnless(len(response.redirect_chain) > 0, ("Response didn't redirect as expected: Response code was %d" " (expected %d)" % (response.status_code, status_code))) self.assertEqual(response.redirect_chain, status_code, ("Initial response didn't redirect as expected: Response code was %d" " (expected %d)" % (response.redirect_chain, status_code))) url, status_code = response.redirect_chain[-1] self.assertEqual(response.status_code, target_status_code, ("Response didn't redirect as expected: Final Response code was %d" " (expected %d)" % (response.status_code, target_status_code))) else: # Not a followed redirect self.assertEqual(response.status_code, status_code, ("Response didn't redirect as expected: Response code was %d" " (expected %d)" % (response.status_code, status_code))) url = response['Location'] scheme, netloc, path, query, fragment = urlsplit(url) redirect_response = response.client.get(path, QueryDict(query)) # Get the redirection page, using the same client that was used # to obtain the original response. self.assertEqual(redirect_response.status_code, target_status_code, ("Couldn't retrieve redirection page '%s': response code was %d" " (expected %d)") % (path, redirect_response.status_code, target_status_code)) e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url) if not (e_scheme or e_netloc): expected_url = urlunsplit(('http', host or 'testserver', e_path, e_query, e_fragment)) self.assertEqual(url, expected_url, "Response redirected to '%s', expected '%s'" % (url, expected_url))
You’ll notice that if your get request uses the follow=False option you’ll end up at line 34 in this code snippet which will kindly check to make sure the page you are redirecting to returns a 200. Which is great, unless you don’t have the correct mocks for that page setup too. Mocking out the content for a page you aren’t actually testing also didn’t seem quite right. We didn’t care about the other page loading, it had it’s own test cases. We just wanted to make sure the page under test was redirecting to where we expected. Simple solution, write our own assertRedirects method.
def assertRedirectsNoFollow(self, response, expected_url): self.assertEqual(response._headers['location'], ('Location', settings.TESTSERVER + expected_url)) self.assertEqual(response.status_code, 302)
Back to a 2 second unit test run time and all is right with the world again.