Archive

Posts Tagged ‘test’

Django’s assertRedirects little gotcha

April 23, 2010 1 comment

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[0][1], status_code,
                ("Initial response didn't redirect as expected: Response code was %d"
                 " (expected %d)" % (response.redirect_chain[0][1], 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.

Dustin Bartlett

Advertisements

Stubbing a Test Mail Server

February 17, 2009 1 comment

Django has built in support for sending email. We make use of this in our app, but when testing, we wanted to be able to access the emails sent so we could assert on their content, and pull data out of the body. In the unit tests, that’s easily solved by mocking the email client call, but we wanted to do this as a black-box regression test. That’s where the Python built in smtpd.SMTPServer comes in handy:

import smtpd, asyncore, threading

email_server = None

class FakeServer(smtpd.SMTPServer):

def __init__(self, localaddr, remoteaddr):
self.server = smtpd.SMTPServer.__init__(self, localaddr, remoteaddr)
self.emails = {}

def process_message(self, peer, mailfrom, rcpttos, data):
for recipient in rcpttos:
idx = recipient.replace(‘@’,’_at_’).replace(‘.’,’_dot_’)
existing_emails = emails.get(idx, [])
existing_emails.append(data)
emails[idx] = existing_emails

def stop(self):
self.server.close()

def main()
email_server = FakeServer((‘localhost’, 10920), None)
def start_email():
asyncore.loop()
thread = threading.Thread(target=start_email)
thread.setDaemon(True)
thread.start()

if __name__==’__main__’:
main()

This starts an email server on port 10910, which on receipt of a new email, stores it in a dictionary keyed by a modified version of the email address. Multiple emails to the same address get appended to the list. There is no reason in this context for replacing the ‘.’ and ‘@’ symbols from the email address in this context, but it was appropriate for our usage.

We run the stub mailserver on a separate box, and make the emails available via a get request on a test HTTP server, which allows us to run tests scripted entirely written in Selenium. I will blog again with details of how we manage our test HTTP server. You can use the this code as the basis for embedding the mail server in a unittest setup too.

By: Chris Tarttelin

Creating Fixtures from Within Tests

February 3, 2009 Comments off

Django gives you a ‘dumpdata’ target which will create a fixture from all the records in your schema. For what we wanted, this was overkill. We had an existing unit test which was creating data in one test, and it was just the right amount of data for what we wanted. After searching through the Django codebase, it became clear that we could pass the objects we had in that test straight to the JSON serializer, and write the output to file. This ended up looking something like this:-

from django.core.serializers import json
serializer = json.Serializer()
objectsToSerialize =
MyModel.objects.filter(column='restriction')
with open('my_fixture.json','w') as f:
. f.write(serializer.serialize(objectsToSerialize, indent=4))

And that’s it! You can also spin up a shell, using:-

python manage.py shell

and load the data you want to create fixtures from if you don’t have any tests that create the data you want already.

By: Chris Tarttelin