Functional tests with lettuce and django test client

on Feb 01, 2013 by Danu


     When running a functional test, you fire up “browser” and do the "same" actions as a real user (or API client). There are different “browsers” for testing your applications, some of them are real, like selenium and some of them are less real, like django test client. Depending on the context, each of them has its pros and cons.

- django test client is very fast, since you don’t need a browser engine, real http listening and so on, but JavaScript and/or Ajax views are not tested

- selenium is a browser-driving library, opens a real browser and tests rendered HTML alongside with behavior of Web pages. Write selenium tests for Ajax, other JS/server interactions.

A complete test suite should contain both test types.

 

Functional tests and Django

Write functional/integration/system (has more names than it needs) tests for views. Unit testing the view is hard because views have many dependencies (templates, db, middleware, url routing etc). 

You should definitely have django functional tests but chances are you should have fewer than you have now.  See the software testing pyramid by Alister Scott which provides solid approach to automated testing and shows the mix of testing a team should aim for.

 

Functional tests:

- test that the whole integrated system works, catch regressions

- they tend to be slow

- will catch bugs that unit tests will not, but it's harder to debug

- write fewer (more unit tests)

 

So what can you test with django-test client ?

- the correct view is executed for a given url

- simulate post, get, head, put etc. requests

- the returned content has the expected values (you can use beautiful soup, lxml or html5lib for parsing the content)

 

Example of functional test with lettuce and django test client

Feature: Register
    In order to get access to app
    A user should be able to register
 
 
Scenario: User registers
    Given I go to the "/register/" URL
    When I fill register form with:
        | username | email       | password1 | password2 |
        | danul    | dan@dan.com | test123   | test123   |
    And I submit the data
    Then I should see "Check your email"
    And I should receive an email at "dan@dan.com" with the subject "Activate your djangoproject.com account - you have 7 days!"
    And I activate the account
    Then I should see "Congratulations!"
 
 
Scenario: Users login
    Given following users exist
      | username | password |
      | danu     | test123  |
      | lulu     | test123  |
    When I go to the "/login/" URL
    And I login as user "danu"
    Then I should see "Welcome, danu. Thanks for logging in."

 

Steps

import re

from bs4 import BeautifulSoup

from lettuce import *
from django.core import mail
from nose.tools import assert_equals

from django.contrib.auth.models import User


@step(u'I go to the "(.*)" URL')
def i_go_to_the_url(step, url):
    response = world.browser.get(url)
    assert_equals(response.status_code, 200)
    world.html = BeautifulSoup(response.content)

@step(u'When I fill register form with:')
def when_i_fill_in_user_data_with(step):
    for data in step.hashes:
        world.data = data
    assert_equals(len(world.html.select('form')), 1)
    assert_equals(len(world.html.find_all('input', 'required')), 4)

@@step(u'And I submit the data')
def and_i_submit_the_data(step):
    world.response = world.browser.post('/register/', world.data, follow=True)
    assert_equals(
        User.objects.filter(username=world.data['username']).exists(), True
    )
    assert_equals(world.response.status_code, 200)

@step(u'I should see "(.*)"')
def i_should_see(step, expected_response):
    html = BeautifulSoup(world.response.content)
    expected_text = html.find('h1').get_text()
    assert_equals(expected_text, expected_response)

@step(u'And I should receive an email at "([^"]*)" with the subject "([^"]*)"')
def i_should_receive_email_with_subject(step, address, subject):
    assert_equals(mail.outbox[0].to[0], address)
    assert_equals(mail.outbox[0].subject, subject)

@step(u'And I activate the account')
def and_i_activate_the_account(step):
    activation_url = re.findall(
        r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+',
        mail.outbox[0].body
    )
    world.response = world.browser.get(activation_url[0], follow=True)
    assert_equals(world.response.status_code, 200)

@step(u'Given following users exist')
def given_following_users_exist(step):
    for user_hash in step.hashes:
        user_data = user_hash.copy()
        password = user_data.pop("password", None)
        user, created = User.objects.get_or_create(**user_data)
        user.set_password(password)
        user.save()

@step(u'And I login as user "([^"]*)"')
def and_i_login_as_user_group1(step, username):
    if not User.objects.filter(username=username).exists():
        User.objects.create_user(
            username=username,
            password='test123',
        )
    assert_equals(len(world.html.select('form')), 1)
    assert_equals(len(world.html.find_all('input')), 3)
    world.response = world.browser.post(
        '/login/',
        {'username': username, 'password': 'test123'},
        follow=True
    )
    assert_equals(world.response.status_code, 200)

 

terrain

import logging

from django.conf import settings
from django.core.management import call_command
from django.test import Client
from django.db import connection
#from django.test.utils import setup_test_enviroment, teardown_test_environment

from lettuce import *


@before.all
def initial_setup():
    logging.info("Loading django's test client ...\n")
    world.browser = Client()

    logging.info("Setting up a test database ...\n")

    try:
        from south.management.commands import patch_for_test_db_setup
        patch_for_test_db_setup()
    except ImportError:
        pass

    # Setup test environment / called by harvest.py
    #setup_test_enviroment()

    world.testdb = connection.creation.create_test_db(
        verbosity=2,
        autoclobber=False,
    )


@after.all
def teardown_browser(total):
    logging.info("Destroy test database ...\n")
    connection.creation.destroy_test_db(world.testdb, verbosity=2)

    # Tear Down the test environment / called by harvest.py
    #teardown_test_enviroment()

 

What's next ?

Well ... WebTest  :) (http://pyvideo.org/video/699/testing-and-django)

Be a good person and write functional tests. Functional testing is something that every app needs, no testing strategy is complete without high-level tests to ensure the entire programming system works together.

 

Source code for this article:

https://github.com/danclaudiupop/django-lettuce-labtests


Tags: test django selenium lettuce bdd client bs4 | Category: testing , python Back To Top


comments powered by Disqus