This past month has been a whirlwind for changes in my testing strategies. I have read countless articles, tutorials and opinions on Django testing, and I think I have finally come to what I consider a good solution.

Testing Structure

After muddling around in just about every django testing strategy, I found a great combination using Factory Boy and Django Webtest. This decision was heavily influenced by a Stack Overflow Question as well as the testing section in Two Scoops of Django.

One great thing I learned from Two Scoops is to split tests up into function sections, such as Models, Views, Forms. In the ModelTests class, you would have tests on model functions, and test custom managers as well. In the views, you test the view logic as well as the permissions. In forms, you test submitting the forms. Other parts of your project should be tested as well, but these three areas will cover most of your testing suite.

Something that I also do is to share the setUp function between all the classes in an appliction's tests because why would we create the model objects multiple times? I create a base class called ModelSetUp, which the other classes inherit from.

Another personal twist worth noting is creating Mixins. I found that I was doing some repeated tasks such as verifying that anonymous users couldn't hit a certain view, but admin users could. I created an AccessMixin to handle that problem. I also created a test application to store my shared testing code, such as the AccessMixin, and all my factory objects.

Creating Objects

There are many pieces to the testing puzzle, and one of those pieces is creating test objects. This is something I really struggled on and I was not following DRY principles until I found out about Factory Boy. Factory Boy creates objects for you, in one place, and you can load those objects wherever you need to.

Oh yeah, it also greatly reduced my testing time when I switched my objects to be created by Factory Boy instead of the Django ORM.

Here are some sample factories that will be referenced later on

from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
from school.models import School
from realestate.models import Company
User = get_user_model()


class SchoolFactory(DjangoModelFactory):
    # need to pass in city
    class Meta:
        model = School

    name="Real Estate Test University"
    long=-97.1234123
    lat=45.7801234


class CompanyFactory(DjangoModelFactory):
    # need to pass in default_school
    class Meta:
        model = Company

    name="Test Company"


class UserFactory(DjangoModelFactory):
    class Meta:
        model = User


class NormalUserFactory(UserFactory):
    username = 'normaluser'
    password = 'normalpassword'
    email = 'user@email.com'
    first_name = 'John'
    last_name = 'Doe'


class RealEstateUserFactory(UserFactory):
    # pass in real_estate_company
    username = 'realestateuser'
    password = 'repassword'
    email = 're@email.com'
    first_name = 'Sir'
    last_name = 'Real'

Testing Examples

I just went through and re-wrote all the tests for the RentVersity project, and I wanted to share a few of my testing strategies. I'm going to use the Real Estate application from RentVersity which will show off all the essential highlights of the testing.

Define the SetUp class that will be inherited

from django.test import TestCase
from django.core.urlresolvers import reverse

from django_webtest import WebTest

from .urls import prefix
from test.factories import CityFactory, SchoolFactory, CompanyFactory, RealEstateUserFactory, \
                           NormalUserFactory
from test.tests import AccessMixin
                           

class RealEstateSetUp(TestCase):
    def setUp(self):
        self.user = NormalUserFactory.create()
        self.city = CityFactory.create()
        self.school = SchoolFactory.create(city=self.city)
        self.company = CompanyFactory.create(default_school=self.school)
        self.real_estate_user = RealEstateUserFactory.create(real_estate_company=self.company)

        self.access_denied_message = "You do not have access"

Ok, so here we have the first part of the Real Estate tests.py which will create all the objects, and will be inherited by all the other application testing classes that need these objects.

As you can see, all the objects are created by factories, which enables me to create the same objects in all of my applications. No more copy and pasting the Model.object.create(arg1, arg2, arg3, arg4.....etc) in each of the applications that needs that object. One beautiful, clean line.

Ok, now for what we've all been waiting for - the actual tests. I test the models first because, well, that just seems like the correct way to go.

class RealEstateModelTest(RealEstateSetUp):
    def test_random_contact(self):
        contact = self.company.get_random_contact()
        
        self.assertEqual(contact, self.real_estate_user)
        self.assertNotEqual(contact, self.user)

Nothing too special in this application, but you should know that this is the one model function to be tested in this application. Many times there will be alot more to test on the models, especially when custom model managers come into play.

Next up are the ViewTests.

class RealEstateViewTest(AccessMixin, RealEstateSetUp, WebTest):
    def test_home(self):
        url = reverse(prefix + 'home')
        anon_response = self.app.get(url)
        self.assertEqual(anon_response.status_code, 200)

    def test_company_home(self):
        url = self.company.get_absolute_url()
        self.real_estate_access(url, self.company.name)

    def test_company_members(self):
        url = reverse(prefix + 'company-members', 
            kwargs={'slug':self.company.slug})
        self.real_estate_access(url, "Member Administration")

    def test_company_properties(self):
        url = reverse(prefix + 'company-properties', 
            kwargs={'slug':self.company.slug})
        self.real_estate_access(url, "Property")

    def test_company_support(self):
        url = reverse(prefix + 'company-support', 
            kwargs={'slug':self.company.slug})
        self.real_estate_access(url, "Support")

Ok, just a few tests but the thing to notice is the AccessMixin. Before developing this mixin, there would be a lot of repeated code for testing if the Real Estate User had access, but a user that a normal user did not. By putting that code into the mixin, I am not only turning 4-8 lines of code into 1, I am also making that same logic available to all the other applications in my project.

Other than the real_estate_access function, there are also other Access related functions to aid my testing suite. 

class AccessMixin(object):
    def staff_required(self, url, text=None):
        """ Run through permission tests that require staff. 
            Sometimes we'll pass in a string and check the response 
            for that string
        """
        anon_response = self.app.get(url)
        self.assertEqual(anon_response.status_code, 302)

        user_response = self.app.get(url, user=self.user)
        self.assertNotEqual(anon_response.status_code, 200)     
        
        staff_response = self.app.get(url, user=self.staff_user)
        self.assertEqual(staff_response.status_code, 200)

    def login_required(self, url):
        """ test login required views and forms
        """
        response = self.app.get(url)
        self.assertEqual(response.status_code, 302)

        response = self.app.get(url, user=self.user)
        self.assertEqual(response.status_code, 200)

    def real_estate_access(self, url, text):
        """ test that the real estate users are the only non-admin users
            that can access their real estate information
        """
        user_response = self.app.get(url, user=self.user)
        assert self.access_denied_message in user_response

        re_response = self.app.get(url, user=self.real_estate_user)
        assert text in re_response

Last but certainly not least, are the FormTests. These tests have definitely given me the most problems out of all the my testing. The forms are a bit tricky in a testing enviornment, however, I think I have them under control now.

Unfortunately there aren't any form tests in the Real Estate application, nor in the RentVersity project, so I'm going to use some Forms from another project called Serendipity Artisan Blends which is an online (and brick and mortar) store that sells custom dips, rubs and marinades.

It might be a different project, but the testing has the same structure. 

class ProductFormTest(ProductSetUp, WebTest):
    def test_product_create_form(self):
        url = reverse(self.prefix + 'create')

        form = self.app.get(url, user=self.staff).form
        form['title'] = 'My Test Mix'
        form['description'] = 'The Test Description'
        form['type'] = '1'
        form['price'] = '5.95'

        response = form.submit().follow()

        # make sure the response has the newly created post
        assert form['title'].value in response

    def test_product_update_form(self):
        url = self.product.get_update_url()

        form = self.app.get(url, user=self.staff).form
        form['title'] = 'Edited Test Mix'
        form['description'] = 'The Test Description'
        form['type'] = '1'
        form['price'] = '5.95'

        response = form.submit().follow()

        # make sure the response has the updated form
        assert form['title'].value in response
        assert self.product.title not in response

Here we're filling out the form field by field, and then using the magic of WebTest to submit the form and follow to the resulting page. We can then verify that the form submitted correctly, as well as checking that the resulting page displays correctly.

And there we have it! A DRY, scalable testing suite with Django.