personal web log written by izabeera and dryobates

django tests

Tests in Django

by dryobates

When doing Test Driven Development, one of the most difficult thing is to find out how to test different technologies. Breaking that barrier makes TDD dead easy. I wrote some tips on how to test different parts of Django projects to help you start with TDD.

I have described testing only the most common and problematic Django project's parts. Many parts are straightforward to test. E.g. middleware's construction can be tested as any other python's class.

Tip

Convensions

Setting up conventions makes easier new programmers to get into project and makes testing easier. For that reason with description how to test different parts of Django project I gave a tips what should be put in this part and what shouldn't.

Full source code of examples from this post is available at github [1]

General tips on testing

As a programmers most often we wrote unit tests and integration/acceptance tests.

Integration tests

Integration tests are responsible for checking that whole code works as expected. Those tests are slow by nature, because in that type of tests it is required to check whole code with external systems like databases, filesystems etc. It is common to check only main paths of control flow. Finding which part of code was the main cause in that type of test is difficult because of weak of that tests.

Unit tests

Unit testing is focused on testing the smallest parts of code. Because that high granularity they allow to point exactly where the problem is. Testing small parts of code makes it easier to check all important paths of control flow. That is the reason why often number of unit tests is huge. While number of tests grows the time of their execution extends. It is important to make them as fast as possible. To achieve this tested code should be separated from any external systems like:

  • databases
  • memcached/redis
  • elasticsearch/solr
  • filesystems
  • celery

Side effect of that separation is that it's easier to switch to some other external system in future.

Doing Test Driven Development helps making more separated and smaller logical units of code which can be tested in separation from other logical units and making test even faster.

Testing views

Any object is easier to test and enhance if it is well separated from their collaborators. In case of view the easiest method is to pass it's collaborators as parameters to method as_view:

class AddTaskViewTest(TestCase):

    def setUp(self):
        self._form_class = Mock(AddTaskForm)
        self._users_manager = Mock(User.objects)
        self._view = AddTaskView.as_view(
            form_class=self._form_class,
            users_manager=self._users_manager)

On above code it is easy to reason that view's collaborators are:

  • manager that provides access to users
  • form class

I used mock library [2] to simulate collaborators as we doesn't need real objects.

When testing views I often write separated test classes for each of their public methods. Below is view with two public methods get and post:

@no_db_testcase
@tags(['unit'])
class AddTaskViewGetTest(AddTaskViewTest):
    ''' :py:meth:`tasks.views.AddTaskView.get` '''

    def test_should_display_task_creation_form(self):
        # Arrange
        url = reverse('my_tasks')
        request = self._factory.get(url)
        request.user = Mock(User)

        # Act
        response = self._view(request)

        # Assert
        self.assertTrue('form' in response.context_data)

First test that I often write checks main control flow. I try to start from easiest to write test in order not to think about code complexity. The main problem at that point is to prepare environment (collaborators) to run test.

In above test I simulate environment where user is already logged in. That is why request.user points to mocked User object.

The only thing I expect from this test is that form should be present in context passed to template.

There are two decorators in above example. no_db_testcase is decorator from django-smarttest [3] which raises an exception when code under test tries to make request to database. I explicit mark tests with that decorator when I expect it shouldn't make queries to database. It allows me to easier detect errors in tests.

Other decorator tags is from Morelia [4] and makes it easier to run tests only with given tags. I mark my tests with tags "unit" (unit tests) and "acceptance" (integration/acceptance tests). Additionally unit tests sometimes are marked with "slow" if I expect them to be slow by definition (e.g. manager's test). "slow" tag allows me to skip them if I'm really in hurry ;)

@no_db_testcase
@tags(['unit'])
class AddTaskViewPostTest(AddTaskViewTest):
    ''' :py:meth:`tasks.views.AddTaskView.post` '''

    def test_should_save_form_and_redirect_on_success(self):
        # Arrange
        url = reverse('my_tasks')
        form = self._form_class.return_value
        form.is_valid.return_value = True
        redirect_url = '/some/url'
        obj = form.save.return_value
        obj.get_absolute_url.return_value = redirect_url
        data = {
            'title': sentinel.title,
        }
        request = self._factory.post(url, data)
        request.user = Mock(User)

        # Act
        response = self._view(request)

        # Assert
        self.assertTrue(form.save.called)
        self.assertTrue(obj.get_absolute_url.called)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response['Location'], redirect_url)

Traditionally every test is consisted with environment preparation code (Arrange), running tested code (Act) and checking results.

When you look at Arrange part it is easy to deduce what collabortors are required. In above example it is form class (self._form_class) which has at least is_valid and save methods. We simmulate that data passed to view are correct so we set up return_value to True.

Everytime when you see in test code:

obj.method.return_value = some_value

It is expected that in production code you'll see something similar to:

variable = obj.method()

Running that code under the test will set variable to some_value. In other words '()' == '.return_value' :)

In example code you can see that it is possible to simulate chain of calls:

obj = form.save.return_value
obj.get_absolute_url.return_value = redirect_url

We expect that method save will be called on form and will return object, that has method get_absolute_url. It is possible to chain more methods this way, but the Law of Demeter [5] suggest that it's not a good idea and if you need something like that then then think on better structuring your code.

The last thing in example is checking results in Assert part. After running tested code we can check:

  • result of invoking method - self.assertEqual(response.status_code, 302)
  • impact on collaborators - self.assertTrue(form.save.called)
  • tested object's state (in above example we do not expect any change in view's attributes)

How do tested view looks?:

from django.contrib.auth.models import User
from django.http import HttpResponseRedirect
from django.views.generic import TemplateView

from .forms import AddTaskForm


class AddTaskView(TemplateView):

    template_name = 'tasks/index.html'
    form_class = AddTaskForm
    users_manager = User.objects

    def get(self, request, *args, **kwargs):
        author = request.user
        owner = self._get_owner()
        form = self.form_class(author=author, owner=owner)
        context = {
            'form': form,
            'owner': owner,
            'author': author,
        }
        return self.render_to_response(context)

    def post(self, request, *args, **kwargs):
        author = request.user
        owner = self._get_owner()
        form = self.form_class(request.POST, author=author, owner=owner)
        if form.is_valid():
            obj = form.save()
            return HttpResponseRedirect(obj.get_absolute_url())
        context = {
            'form': form,
            'owner': owner,
            'author': author,
        }
        return self.render_to_response(context)

    def _get_owner(self):
        try:
            profile_login = self.kwargs['profile']
        except KeyError:
            owner = self.request.user
        else:
            owner = self.users_manager.get(username=profile_login)
        return owner

One thing worth remembering is that we need to declare on view's class attributes that we expect to pas through as_view method.

Because views often have many collaborators it is wise to restrict their logic to minimum. This will make testing easier and the only difficulty will be preparing collaborators. Important side effect is ability to easier extension or reuse that view like it was done below:

from django.conf.urls import url

from .forms import RestrictedAddTaskForm
from .views import AddTaskView


urlpatterns = [
    url(r'^(?P<profile>\w+)/$', AddTaskView.as_view(form_class=RestrictedAddTaskForm), name='profile_tasks'),
    url(r'^$', AddTaskView.as_view(), name='my_tasks'),
]

Designing views it is worth to know their responsibilities:

Tip

View's responsibilities

  • gathers data to render with use of manager's methods
  • initiates forms
  • renders templates

In views you shouldn't

  • validate data - it is form's responsibility
  • save data - it is form's responsibility
  • build complex queries - it is manager's responsibility

Testing forms

In many cases testing form's methods doesn't involve using database. The most convenient way to check form's validation methods is to run is_valid and then check cleaned_data or errors attributes.

class AddTaskFormTest(TestCase):

    def setUp(self):
        self._title = 'some title'
        self._priority = 5
        self._data = {
            'title': self._title,
            'priority': self._priority,
        }


@no_db_testcase
@tags(['unit'])
class AddTaskFormIsValidTest(AddTaskFormTest):
    """ :py:meth:`tasks.forms.AddTaskForm.is_valid` """

    def test_should_validate_input(self):
        # Arrange
        owner = UserFactory.build()
        form = AddTaskForm(self._data, author=owner, owner=owner)

        # Act
        result = form.is_valid()

        # Assert
        self.assertTrue(result)
        self.assertEqual(form.cleaned_data['title'], self._title)
        self.assertEqual(form.cleaned_data['priority'], self._priority)

Similarly we can check errors dict when we expect error:

def test_should_return_not_valid_if_too_many_tasks(self):
    # Arrange
    author = UserFactory.build()
    owner = UserFactory.build()
    tasks_manager = Mock(Task.objects)
    tasks_manager.get_for_owner_by_author.return_value.count.return_value = TASKS_LIMIT + 1
    form = RestrictedAddTaskForm(
        self._data, author=author, owner=owner,
        tasks_manager=tasks_manager)

    # Act
    result = form.is_valid()

    # Assert
    self.assertFalse(result)
    msg = 'You have added to many tasks for %s' % owner.username
    self.assertTrue(msg in form.errors['__all__'])

In most cases save method writes something to databases, so testing it in isolation from databases has no sense:

@tags(['unit', 'slow'])
class RestrictedAddTaskFormSaveTest(RestrictedAddTaskFormTest):
    """ :py:meth:`tasks.forms.RestrictedAddTaskForm.save` """

    def test_should_create_task_with_default_priority(self):
        # Arrange
        author = UserFactory.create()
        owner = UserFactory.create()
        tasks_manager = Mock(Task.objects)
        form = RestrictedAddTaskForm(
            self._data, author=author, owner=owner,
            tasks_manager=tasks_manager)
        author_tasks = tasks_manager.get_for_owner_by_author.return_value
        author_tasks.count.return_value = 2

        # Act
        form.is_valid()
        obj = form.save()

        # Assert
        self.assertIsNotNone(obj.pk)
        self.assertEqual(obj.title, self._title)
        self.assertEqual(obj.priority, 4)
        self.assertEqual(obj.author, author)
        self.assertEqual(obj.owner, owner)

In above example I use factory-boy [6] for easier creation of related object User:

class UserFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = User

    id = factory.Sequence(lambda n: n)
    username = factory.Sequence(lambda n: 'user{0}'.format(n))
    email = factory.LazyAttribute(lambda a: '{0}@example.com'.format(a.username).lower())
    password = make_password('test')

With form like with any other object it is good practice not to tightly couple objects. In views we have used as_view method to pass collaborators. In case of forms we can use __init__:

class RestrictedAddTaskForm(AddTaskForm):

    class Meta:
        model = Task
        fields = ('title',)

    def __init__(self, *args, **kwargs):
        self._tasks_manager = kwargs.pop('tasks_manager', Task.objects)
        super(RestrictedAddTaskForm, self).__init__(*args, **kwargs)

    def _set_default_attributes(self, obj):
        obj = super(RestrictedAddTaskForm, self)._set_default_attributes(obj)
        tasks_num = self._tasks_manager.get_for_owner_by_author(
            self._owner, self._author).count()
        obj.priority = PRIORITY_BASE ** tasks_num
        return obj

Tip

Form responsibilities

  • validate data
  • save changes

In forms you shouldn't

  • render templates - it is view's responsibility
  • build complex queries - it is manager's responsibility

Testing managers

Managers are those layer that communicate directly with database. Code in manager is responsible for building queries. For that reason manager's tests have to use database. I mark such tests with "slow" tag.

@tags(['unit', 'slow'])
class TaskManagerGetForOwnerByAuthorTest(TestCase):
    """ :py:meth:`tasks.managers.TaskManager.get_for_owner_by_author` """

    def setUp(self):
        self._owner = UserFactory.create()
        self._num = 10

    def test_should_return_own_tasks(self):
        # Arrange
        TaskFactory.create_batch(
            self._num,
            owner=self._owner,
            author=self._owner)

        # Act
        result = Task.objects.get_for_owner_by_author(self._owner, self._owner)

        # Assert - should validate
        self.assertEqual(len(result), self._num)
        for task in result:
            self.assertEqual(task.owner, self._owner)
            self.assertEqual(task.author, self._owner)

In case of managers it is convenient to use libraries like factory-boy for object's creation. It is easy to create many objects according to defined rules. Increasing number of objects can also help to observe how their number can impact performance.

from django.db.models import Manager


class TaskManager(Manager):

    def get_for_owner_by_author(self, owner, author):
        return self.get_queryset().filter(owner=owner, author=author)

    def get_for_owner(self, owner):
        return self.get_queryset().filter(owner=owner)

Tip

Manager responsibilities

  • get objects from database
  • create and destroy objects in database

Testing templatetags

Templatetags are views in mini scale and have similar responsibilities, but are tested different.

In my work one of the most often used templatetags are inclusion_tags. Django's tags registration mechanism makes testing templatetags a little more difficult. Especially mocking objects can be more demanding. Because of that it is convenient to extract templatetag function's body to separate function, and test only that function:

from django import template

from tasks.models import Task


register = template.Library()


def _show_current_tasks(profile, user, tasks_manager=Task.objects):
    tasks = tasks_manager.get_for_owner(profile)
    return {'tasks': tasks, 'user': user}


@register.inclusion_tag('tasks/show_current_tasks.html')
def show_current_tasks(profile, user):
    return _show_current_tasks(profile, user)

And test:

@tags(['unit'])
class ShowCurrentTasksTest(TestCase):
    """ :py:func:`tasks.templatetags.tasks_tags._show_current_tasks` """

    def test_should_show_tasks_for_profile(self):
        # Arrange
        tasks_manager = Mock(Task.objects)
        tasks_manager.get_for_owner.return_value = sentinel.current_tasks

        # Act
        result = _show_current_tasks(
            sentinel.profile,
            sentinel.user,
            tasks_manager=tasks_manager)

        # Assert
        self.assertEqual(result['user'], sentinel.user)
        self.assertEqual(result['tasks'], sentinel.current_tasks)
        tasks_manager.get_for_owner.assert_called_once_with(sentinel.profile)

In above example there is only one collaborator tasks_manager and 2 parameters.

We do not expect any interaction with this parameters so we can use sentinel objects and check that they were passed unchanged to template.

In case of tasks_manager we expect that it's method get_for_owner will be called with passed sentinel.profile object, so we check for that interaction in Assert part.

Testing filters is straightforward. We can test them like any other function:

@tags(['unit'])
class IsVisibleForTest(TestCase):
    """ :py:func:`tasks.templatetags.tasks_tags.is_visible_for` """

    def test_should_return_true_for_author_tasks(self):
        # Arrange
        task = Mock(Task)
        task.author = sentinel.author
        task.owner = sentinel.owner

        # Act
        result = is_visible_for(task, sentinel.author)

        # Assert
        self.assertTrue(result)

# ...

@register.filter
def is_visible_for(task, user):
    return user in [task.owner, task.author]

Tip

Templatetag responsibilities

  • gathers data to render with use of manager's methods
  • initiates forms - renders templates

In forms you shouldn't

  • validate data - it is form's responsibility
  • save data - it is form's responsibility
  • build complex queries - it is manager's responsibility

Testing models

Most model's methods doesn't require database while tested, so we can create model objects without saving it:

@no_db_testcase
@tags(['unit'])
class TaskGetAbsoluteUrlTest(TestCase):
    ''' :py:meth:`tasks.models.Task.get_absolute_url` '''

    def test_should_return_task_absolute_url(self):
        # Arrange
        owner = UserFactory.build(pk=1)
        task = TaskFactory.build(owner=owner, author=owner)

        # Act
        url = task.get_absolute_url()

        # Assert
        self.assertEqual(url, '/%s/' % owner.username)

In above example we have created object with factory-boy, but doesn't save it to database (build method doesn't save object in contrary to create). We can achieve the same without using factory-boy simply instantiating object:

task = Task(owner=owner, author=owner)

Tip

Model responsibilities

  • define object fields
  • return instance data and related objects
  • modify internal instance attributs

In models you shouldn't

  • build complex queries - it is manager's responsibility - render templates - it is view's responsibility

Model's aren't restricted only to database objects. In models.py should be put all objects representing data e.g. from redis.

It is good practice to separate method's code to part that requires database access from the rest. It makes testing simpler.

Testing templates

Templates are hard to test and debug. It is hard to isolate it from used templatetags and sometimes it is hard to check that rendered template contains required data.

Djano's team put great effort to make templating system easy to extend and at the same time making it less tempting to put too much logic in it. If you feel that something is hard to perform in temmplate stop and think if this really should go to template? Maybe it's better to put into templatetag?

In order to test template we can use render_to_string shortcut and check resulting string.

from django.test import TestCase
from django.template.loader import render_to_string

from morelia.decorators import tags

from tasks.factories import UserFactory, TaskFactory


@tags(['unit'])
class ShowCurrentTasksTest(TestCase):
    """ tasks/show_current_tasks.html """

    def test_should_show_only_author_tasks_on_foreign_profile(self):
        # Arrange
        template_name = 'tasks/show_current_tasks.html'
        owner = UserFactory.build()
        wife = UserFactory.build()
        bread = "buy bread"
        milk = "buy milk"
        tasks = [
            TaskFactory.build(title=bread, owner=owner, author=owner),
            TaskFactory.build(title=milk, owner=owner, author=wife),
        ]
        context = {
            'tasks': tasks,
            'user': wife,
        }
        # Act
        result = render_to_string(template_name, context)

        # Assert
        self.assertFalse("buy bread" in result)
        self.assertTrue("buy milk" in result)
{% load tasks_tags %}

<ul id="todo">
    {% for task in tasks %}
        <li>{% if task|is_visible_for:user %}{{ task.title }}{% else %}******{% endif %}</li>
    {% endfor %}
</ul>

Tip

Templates responsibilities

  • displaying data

In templates you shouldn't

  • put more logic then is required to format data for display

Doing integration/acceptance tests

Unit test doesn't guarantee that code will be coherent so it is required to use them with integration tests.

For acceptance tests I write scenarios in gherkin language [7]:

Feature: Add task

    As logged user
    In order to not forget what I need to do
    I want to add task to todo list

Scenario: Adding task

    Given user "test1" exists
    When I visit "/" as logged user "test1"
    And I enter "buy bread" in field "title"
    And I enter "5" in field "priority"
    And I press button "submit"
    Then I see task "buy bread" on tasks list

And with morelia [4] I integrate it with standard python's tests:

import os

from django.test import TestCase

from morelia import run
from morelia.decorators import tags
from splinter import Browser

from tasks.factories import UserFactory


@tags(['acceptance'])
class AddTask(TestCase):

    def setUp(self):
        self._browser = Browser('django')

    def test_add_task(self):
        filename = os.path.join(os.path.dirname(__file__),
                                '../../docs/features/add_task.feature')
        run(filename, self, verbose=True)

    def step_user_exists(self, username):
        r'user "([^"]+)" exists'

        user = UserFactory.build(username=username)
        user.is_staff = True
        user.set_password(username)
        user.save()

    def step_I_visit_page_as_logged_user(self, page, username):
        r'I visit "([^"]+)" as logged user "([^"]+)"'

        self._browser.visit('/admin/')
        self._browser.fill('username', username)
        self._browser.fill('password', username)
        self._browser.find_by_value('Log in').first.click()
        self._browser.visit(page)

    def step_I_enter_value_in_field(self, value, field):
        r'I enter "([^"]+)" in field "([^"]+)"'

        self._browser.fill(field, value)

    def step_I_press(self, button):
        r'I press button "([^"]+)"'

        self._browser.find_by_name(button).first.click()

    def step_I_see_task_on_tasks_list(self, task):
        r'I see task "([^"]+)" on tasks list'

        task_on_list = self._browser.find_by_xpath('//ul[@id="todo"]/li[contains(., "%s")]' % task)
        self.assertTrue(task_on_list)
[1]https://github.com/dryobates/testing_django
[2]mock http://www.voidspace.org.uk/python/mock/
[3]django-smarttest https://pypi.python.org/pypi/django-smarttest
[4](1, 2) Morelia http://morelia.readthedocs.org/en/latest/
[5]Law of Demeter https://en.wikipedia.org/wiki/Law_of_Demeter
[6]Factory-boy https://factoryboy.readthedocs.org/en/latest/
[7]Gherkin http://morelia.readthedocs.org/en/latest/gherkin.html
dryobates
dryobates
Jakub Stolarski. Software engineer. I work professionally as programmer since 2005. Speeding up software development with Test Driven Development, task automation and optimization for performance are things that focus my mind from my early career up to now. If you ask me for my religion: Python, Vim and FreeBSD are my trinity ;) Email: jakub@stolarscy.com

Archive

Tag cloud