Michał Karzyński

Performance testing Django applications with The Grinder

Performance testing web applications is a little tricky when we want to test features accessible only to logged in users. Automated test recorders intercept session cookies and hardcode them into test scenarios. When we run these tests later, different session IDs are generated and recorded cookie values don’t grant access anymore. This text demonstrates how to write a scenario for The Grinder to test a Django application. Tests include user login, submitting CSRF-protected forms and AJAX requests.

Test scenario example

Let’s dive right in. The code below is a sample test scenario for The Grinder. It represents various aspects of interaction between the test agent and your application including:

  • logging a user in
  • accessing password protected pages and API calls:
    • GETting a page
    • submitting a CSRF-protected form
    • sending an AJAX XMLHttpRequest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
from net.grinder.script.Grinder import grinder
from net.grinder.script import Test
from net.grinder.plugin.http import HTTPRequest, HTTPPluginControl
from HTTPClient import Cookie, CookieModule, CookiePolicyHandler, NVPair

HOST_DOMAIN = 'example.com'
HOST_URL = 'http://%s' % HOST_DOMAIN


def create_request(test, headers=None):
    request = HTTPRequest()
    if headers:
        request.headers = headers
    test.record(request)
    return request


def get_csrf_token(thread_context):
    cookies = CookieModule.listAllCookies(thread_context)
    csrftoken = ''
    for cookie in cookies:
        if cookie.getName() == 'csrftoken':
            csrftoken = cookie.getValue()
    return csrftoken


class TestRunner:
    def __call__(self):
        print "O, hai!"

        thread_context = HTTPPluginControl.getThreadHTTPClientContext()

        create_request(Test(1100, 'Connect to host')).GET(HOST_URL + '/')

        create_request(Test(1200, 'Get login page')).GET(HOST_URL + '/login/')

        # Log a user in using username, password and CSRF token read from cookie
        create_request(Test(1300, 'Log in user')).POST(HOST_URL + '/login/', (
            NVPair('csrfmiddlewaretoken', get_csrf_token(thread_context)),
            NVPair('username', 'michal'),
            NVPair('password', 'myfakepassword'),
            NVPair('next', '%2F'),))

        create_request(Test(1400, 'Access page requiring login')).GET(HOST_URL + '/user/area/')

        create_request(Test(1500, 'Post to form requiring login')).POST(HOST_URL + '/user/area/action', (
            NVPair('csrfmiddlewaretoken', get_csrf_token(thread_context)),
            NVPair('param1', 'value'),
            NVPair('param2', 'value')))

        # Prepare an "AJAX" request with appropriate headers, indluding CSRF token from cookie
        ajax_request = create_request(Test(1600, 'Send an AJAX request requiring login'), [
            NVPair('X-Requested-With', 'XMLHttpRequest'),
            NVPair('X-CSRFToken', get_csrf_token(thread_context)),
        ])
        ajax_request.POST(HOST_URL + '/user/area/action', (
            NVPair('param1', 'value'),
            NVPair('param2', 'value')))

        print "Kthnxbye!"

How does it work?

Django as well as many other web applications use a simple authentication mechanism. First, the user provides a username and password combination. The application then verifies user data, opens a session and sends a session cookie back to the user. A session ID contained in the cookie uniquely identifies the user’s open session. As long as each request from the user comes in accompanied by the cookie, the user is considered logged in, at least until the session expires on the server.

The example test scenario file above depends on The Grinder’s ability to parse and resubmit cookies, so you don’t actually have to worry about the sessionid cookie.

A note on CSRF tokens and testing AJAX methods

Another obstacle to overcome when running automated tests on Django are anti cross-site request forgery tokens. These tokens are generated dynamically for every form and Django requires that they be submitted with every POST request.

In the example test scenario we don’t parse HTML forms, but instead rely on the fact that Django also sets a cookie with the CSRF token value. We fetch the cookie value (using the get_csrf_token function) and submit it as a field named csrfmiddlewaretoken in POST data.

1
2
3
4
5
create_request(Test(1500, 'Post to form requiring login')).POST(HOST_URL + '/user/area/action', (
    NVPair('csrfmiddlewaretoken', get_csrf_token(thread_context)),
    NVPair('param1', 'value'),
    NVPair('param2', 'value'),
))

We can also simulate AJAX requests by sending appropriate headers, namely X-Requested-With and X-CSRFToken. The anti-CSRF token value is read from cookie and written in the latter header.

1
2
3
4
ajax_request = create_request(Test(1600, 'Send an AJAX request requiring login'), [
    NVPair('X-Requested-With', 'XMLHttpRequest'),
    NVPair('X-CSRFToken', get_csrf_token(thread_context)),
])

If you’re still having trouble with CSRF and keep testing 403 errors instead of your application, you can disable CSRF completely using a small bit of middleware. Just make sure you don’t leave this on in production.

1
2
3
class StressTestingMiddleware(object):
    def process_request(self, request):
        setattr(request, '_dont_enforce_csrf_checks', True)

Final result

When your tests are prepared and properly vetted you can leverage the power of The Grinder to run them from as many parallel agent machines (and/or threads) as you require. Your test output will include execution time statistics for every test step.

Partial output of a sample test run

Comments