Testing for Django Rest Framework with Factory Boy and Faker

Introduction

“I’m not a great programmer; I’m just a good programmer with great habits.” ― Kent Beck

Test Driven Development - in short TDD is a practice where you write the tests first then the actual code. It makes the code less vulnerable and it makes a testable software rather than writing the software, then test it.

Django Rest Framework - in short DRF is a powerful framework which provides RESTful API support over Django. It provides minimal ways to expose the RESTful API directly from model( or without it, along with many other features out-of-the box like authentication, permission, throttling and so on.).

As we will be testing our RESTful APIs though TDD, we need data which should simulate real world data. But writing these data in the code may not be efficient if its in large volume. Also it may take a long time to do so. So we need some tools which will generate those fake data for us. That is where Factory Boy and Faker comes in.

Today we are going to discuss how we can use TDD in DRF with shortest implementation possible.

Preparation

As always, we need to prepare ourselves before starting something. Obviously we need to setup Django and DRF in our local machine. Now, we can write a model in our system like this:

class Blog(models.Model):
    name = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255)
    body = models.TextField()
    # ... and so on

Then, we need to write a serializer for serializing the Blog objects:

class BlogSerializer(serializer.ModelSerializer):
    class Meta:
        model = Blog

Let’s write a small view, which takes slug as url parameter and returns the actual blog response:

from rest_framework.views import APIView
from rest_framework.response import Response


class BlogView(APIView):
    def get(self, slug):
        try:
             blog = Blog.objects.get(slug=slug)
             serializer = BlogSerializer(blog)
             return Response(serializer.data, status=status.HTTP_200_OK)
        except BlogDoesNotExist:
            return Response(status=status.HTTP_404_NOT_FOUND)

        except Exception:
            return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)

Another view to get all Blogs:

from rest_framework.views import APIView
from rest_framework.response import Response


class BlogListView(APIView):
    def get(self):
        try:
             blogs = Blog.objects.all()
             serializer = BlogSerializer(blog, many=True)
             return Response(serializer.data, status=status.HTTP_200_OK)
        except Exception:
            return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)

Finally attach this view to an URL:

path('blog/<slug:title>/', BlogView.as_view(), name="blog-get"),
path('blogs/', BlogView.as_view(), name="blog-all"),

Thus our preparation for testing is complete. Now lets dive into testing.

Testing

For writing test cases, we need to create some files inside the app. Lets say, we have created test_blog.py and inside it, the code should look like this:

from rest_framework.test import APITestCase


class BlogTest(APITestCase):
   ...

Here, we will be subclassing BlogTest from APITestCase because it comes built in with self.client which can be used for calling the API. Now lets setup a test:

from django.urls import reverse  # https://docs.djangoproject.com/en/2.1/ref/urlresolvers/#reverse

class BlogTest(APITestCase):
   def test_blog_not_found(self):
       data = {'slug': 'random'}
       response = self.client.get(reverse('blog-get'), data=data)
       self.assertEqual(response.status_code, status.HTTP_400_NOT_FOUND)

Here we can see, if we call the API without creating a blog instance, it will return 404. Now lets create one:

def test_blog_found(self):
    name = "TEST"
    slug = "test"
    body = "TEST TWO"
    blog = Blog.objects.create(name=name, body=body, slug=slug)
    ata = {'slug': slug}
    response = self.client.get(reverse('blog-get'), data=data)
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertEqual(response.json().get('slug'), slug)
    ...

In this case, we should get 200 status and response slug should match our provided slug. As we are testing only one object, creating one should not be a problem. But how about a list objects(regarding our second view). Can we write the name, slug, body variables for 100 times if we need to check for 100 objects?

To overcome this, we can setup a library called factory boy which will generate Blog objects randomly. To install factory boy, we need to type:

pip install factory_boy

Now, to generate 100 Blog objects, we can setup a Factory Class which has Blog in Meta class:

class BlogFactory(factory.Factory):
    class Meta:
        model = Blog

To ensure each object is random, we can put some providers in attributes(named same as model fields):

class BlogFactory(factory.Factory):
    class Meta:
        model = Blog
    name = factory.Faker('name')
    slug = factory.Faker('slug')
    body = factory.Faker('text')

Now we can use this factory in our test:

def test_hundrand_blogs(self):
    for i in range(100):
        BlogFactory()

    response = self.client.get(reverse('blog-list'))
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertEqual(len(response.data), 100)
    ...

Pretty cool, right!! Now, let’s say we want to create a BlogFactory instance which has title X. We can do that by passing title keyword argument through BlogFactory instance call:

def test_blog_X(self):
    blog = BlogFactory(title='X')
    response = self.client.get(reverse('blog-get'), data={'slug': blog.slug})
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertEqual(response.json().get('title'), "X")
    ...

Finally, one more test case, where you want the X to be a random value. Its also doable using faker. We can install it using:

pip install Faker

And we can use it like this:

from faker import Faker

...
def test_blog_fake_name(self):
    fake = Faker()

    name = fake.name()
    blog = BlogFactory(title=name)
    response = self.client.get(reverse('blog-get'), data={'slug': blog.slug})
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.assertEqual(response.json().get('title'), name)

Conclusion

Testing is made easier by libraries like Factory boy, Faker etc. So we should use them write test cases faster.

Thats it. Thank you for reading. Let me know if you have any questions in comments section below.

Cheers!!