🇧🇷 Leia em Português

My favorite testing tools on Django

In today’s development, tests are a fundamental tool for keeping things nice and easy and to keep programmer’s sanity. I’ve been using a set of tools for developing my web applications with Django and it is time for me to share a little bit about them.

Changing an untested code

To exemplify what we can do with the tools, we will use a Django project that has the two models below: Student and Parent. Parent is a simple model with only two attributes, but Student has a lot of attributes to fill in.

from django.db import models


class Parent(models.Model):
    first_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)


class Student(models.Model):
    first_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)
    address = models.CharField(max_length=300)
    resume = models.TextField()
    age = models.PositiveSmallIntegerField()
    email = models.CharField(max_length=250)
    date_started = models.DateTimeField()
    gender = models.CharField(max_length=200)
    parent = models.ForeignKey(Parent, related_name='children',
                               on_delete=models.CASCADE)

Remark: The full project code is available here for reference :)

Lazy shell

Lazy

Once we have our model setup and ou database up and running, we can check what’s hapenning on our database using Django default shell by running (using pipenv):

$ pipenv run school/manage.py shell

After this, shell prompt is available for us. We can them import our model Parent, for instance, and check how many instances we already have on our database. Since we just started things up, no entries so far.

Now imagine that you are working with a big project with lots of model classes to import. Importing every single model class becomes kind of a boring and time consuming task. It’s time for something better.

I use django_extensions to help me deal with this. By default all model classes are imported. It has a lot of cool stuff too but, for me, just the imports are enough to make it crucial for me to use it on day-to-day development.

Once you’ve installed the lib, just add it to the INSTALLED_APPS:

# settings.py

INSTALLED_APPS = [
    ...

    # 3rd parties
    'django_extensions', 
]

To access your new improved shell, just type:

pipenv run school/manage.py shell_plus

Done and done… on the first line you can already check how’s many instances you already have:

Configuring Pytest

To test your code you should really, really look into pytest, which is a great tool specialized in tests. Pytest is a framework, full of extensions and tricks that are way far the scope of this post. Bruno Oliveira just launched his new book to help you learn it :) Check it out if you like what you see here, ok?

To configure pytest to work with Django, create a file pytest.ini in the same folder of manage.py. The file must be similar to this:

# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = school.settings
python_files = tests.py test_*.py *_tests.py

Don’t forget to change school to your project’s name, ok? :)

Let’s try it out?

$ pipenv run pytest

The result is pretty but no tests were found…

Let’s check if it’s working… create a folder tests on our app student and add a file tests.py. Don’t forget to add an empty file __init__.py on that same folder, so pytest is able to found the folder. In the tests.py file, we create a simple test that will fail for sure, just to get things going…

# student/tests/tests.py

def test_something():
    assert True == False

Run again and… voilá! It found the test and a red alert is printed in our screen.

If we fix the test to assert a true comparisson, then everything is clean and green:

Lazy records

To have something to test, let’s use a simple endpoint example that shows details of a specific student. We will use the lib restless to this, the same one I showed how to write endpoints on this post.

You can see on the preparer variable that we will only show our student first name on the api response:

from restless.dj import DjangoResource
from restless.preparers import FieldsPreparer

from .models import Student


class StudentResource(DjangoResource):
    preparer = FieldsPreparer(fields={
        'name': 'first_name',
    })

    def detail(self, pk):
        return Student.objects.get(pk=pk)

Let’s test our brand new endpoint… and we have a problem: there’s nothing on our database to test the response:

We can open our brand new shell_plus and start adding stuff. Well, Student depends on a Parent instance, so first we add a new parent. We can’t forget to save it, otherwise it won’t work (trust me, I did this while writing this). Now we have a ton of information we have to come up with to make it a new database record. And again… don’t forget to save it.

Now we could manually test our api:

So things are working but things are pretty manual. As a project goes bigger, testing things becomes more and more difficult and more instances are requeried to test multiple features in this complex world. Thus, writing everything by hand truly doesn’t seem like a good approach.

I use a lib called factory-boy to come up with this information for me. Let’s start with the Parent model. We create a class called ParentFactory that inherits the factory.DjangoModelFactory. On the class Meta we tell this factory, which model will this factory refer to. Now we take all atributes of the model and add it on this factory using this factory.Faker. Yes, it will come up with names in a coherent manner and you don’t have to worry about names or remember a cool character from Game of Thrones to add its name here.

# student/tests/factories.py

import factory
from student.models import Parent

class ParentFactory(factory.DjangoModelFactory):
    class Meta:
        model = Parent

    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')

Now let’s see… we start with no record of Parent on our database. After we instantiate our ParentFactory we have a new record saved on our database. Pretty nice! And you can see now that this new parent is called Karen Palmer, that is, our factory created a new instance on the database with a normal name (not just a bunch of letters together).

Now we can do the same with the Student class. Factory boy have a lot of tools that can help you on this task: Fakers for first name, last name, address and text, random integer, create emails based on the instance first and last name, and random choice for the gender.

We need not only a lot of information, but we also need another instance from the database. For this, we add it as a Subfactory of the ParentFactory. This ensures that everytime a new Student instance is created, a Parent instance is created with it to fill this necessity.

class StudentFactory(factory.DjangoModelFactory):
    class Meta:
        model = Student

    parent = factory.SubFactory(ParentFactory)
    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')
    address = factory.Faker('address')
    resume = factory.Faker('text')
    age = fuzzy.FuzzyInteger(6, 12)
    email = factory.LazyAttribute(
        lambda o: f'{o.first_name.lower()}.{o.last_name.lower()}@mail.org')
    date_started = fuzzy.FuzzyDateTime(datetime.now(tz=utc))
    gender = fuzzy.FuzzyChoice(['male', 'female', 'other'])

Now let’s test it. We create a new Student and although our database already have a Parent instance, the factory create a new parent to associate it with this new instance of Student that was just created. And notice that it doesn’t get the old Parent we created earlier, it is a brand new instance:

Now, if we want to create a sibling for this previous student, we only need to pass to the new StudentFactory will will create an already created instance of Parent. This way, it will not create a new instance but rather add the instance you just passed to it. Now we kept the same number of parents we already had on our database but now we have two students with the same parent:

So, this already makes our life pretty easy… but there’s more! It can also create batches of instances. So you can actually populate your database in a single line!

Lazy as can be, right? :)

Automated test of the api

We tested our endpoint manually, but that’s not the right way to do this. We have to create a unitary test to make sure it is working now and it will be in the future when we add more stuff to our project. To test the endpoint we will need to use Django client (or something similar). To use a client with pytest is super hard. Just kidding :) We actually just have to install pytest_django and that’s it.

Now, we just create a test and pass a parameter client to the function. That’s it. Pytest will work his magic for you. Now we have our client up and running for testing. On this test we create a new instance of Student, used the client.get method to access the endpoint. The url is mounted using the id of the instance we just created.

For now, let’s just make sure that our response get’s a 200 code:

# student/tests/tests.py

import pytest
from .factories import StudentFactory

@pytest.mark.django_db
def test_endpoint(client):
    student = StudentFactory()
    response = client.get(f'/api/{student.id}', follow=True)
    assert response.status_code == 200

Did you notice that above our test function we had a decorator @pytest.mark.django_db? This is a helper and it is necessary to mark a that this test function is requiring the database. It will ensure the database is set up correctly for the test. More information on helpers can be seen here.

We can also load the response content using the lib json and be sure that the name the api returned is the name we actually want.

import json
import pytest
from .factories import StudentFactory

@pytest.mark.django_db
def test_endpoint(client):
    student = StudentFactory()
    response = client.get(f'/api/{student.id}', follow=True)
    content = json.loads(response.content)
    assert response.status_code == 200
    assert content['name'] == student.first_name

Now imagine that we have several tests that need this student record to be created. It is a waste to keep creating it over and over again. Pytest allows you to create fixtures, which are functions that can be added on you test. For instance, in the code bellow you see that we create a fixture that return the instance. To use it on our test, just add user (the function name) as an input parameter of our test. The only thing is that the client must be the first parameter. Now, we don’t need to create this student anymore in any other line. The student instance will be available on this and any other test that add the function user to the test function.

import json
import pytest

from .factories import StudentFactory

@pytest.fixture
def user():
    return StudentFactory()


@pytest.mark.django_db
def test_endpoint(client, user):
    response = client.get(f'/api/{user.id}', follow=True)
    content = json.loads(response.content)
    assert response.status_code == 200
    assert content['name'] == student.first_name

And if you ever need to use ipdb with it, just run pytest with an extra parameter: pipenv run pytest -s

This is it… with the combo django_extensions + factory boy + pytest testing becomes a really fun thing to do :)


Cheers!
Letícia

Comments