Skip to main content

Django REST Testing

Introduction

Testing is a crucial part of developing robust and reliable APIs with Django REST Framework (DRF). Well-tested code reduces bugs, ensures your API behaves as expected, and makes it easier to maintain and extend your codebase. This guide will introduce you to testing in Django REST Framework, walking through the fundamentals and providing practical examples to help you get started.

Django REST Framework builds on Django's existing test framework, which is built on Python's standard unittest module. By the end of this tutorial, you'll understand how to write tests for your API endpoints, validate responses, and ensure your API is working as expected.

Getting Started with API Testing

Prerequisites

Before we dive into testing, ensure you have:

  1. A Django project with Django REST Framework installed
  2. Basic knowledge of Django models and DRF serializers/views
  3. Understanding of Python's testing concepts

Setting Up Your Testing Environment

Django provides a built-in TestCase class, but for REST Framework testing, we'll use DRF's enhanced APITestCase which is specifically designed for testing APIs.

First, let's create a simple model and API to test:

python
# models.py
from django.db import models

class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=100)
published_date = models.DateField()
isbn = models.CharField(max_length=13)

def __str__(self):
return self.title
python
# serializers.py
from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['id', 'title', 'author', 'published_date', 'isbn']
python
# views.py
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
python
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BookViewSet

router = DefaultRouter()
router.register(r'books', BookViewSet)

urlpatterns = [
path('api/', include(router.urls)),
]

Writing Your First API Test

Now let's create a test file to test our Book API:

python
# tests.py
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Book
import datetime

class BookAPITests(APITestCase):
def setUp(self):
# This method will run before every test
self.book = Book.objects.create(
title='Test Book',
author='Test Author',
published_date=datetime.date(2020, 1, 1),
isbn='1234567890123'
)

def test_get_book_list(self):
"""
Ensure we can retrieve the book list.
"""
url = reverse('book-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['title'], 'Test Book')

Testing API Endpoints

Let's extend our tests to cover all CRUD operations:

python
def test_create_book(self):
"""
Ensure we can create a new book.
"""
url = reverse('book-list')
data = {
'title': 'New Book',
'author': 'New Author',
'published_date': '2021-01-01',
'isbn': '9876543210123'
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Book.objects.count(), 2)
self.assertEqual(Book.objects.get(pk=response.data['id']).title, 'New Book')

def test_get_book_detail(self):
"""
Ensure we can retrieve a specific book.
"""
url = reverse('book-detail', args=[self.book.id])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Test Book')

def test_update_book(self):
"""
Ensure we can update a book.
"""
url = reverse('book-detail', args=[self.book.id])
data = {
'title': 'Updated Book',
'author': 'Test Author',
'published_date': '2020-01-01',
'isbn': '1234567890123'
}
response = self.client.put(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Updated Book')

def test_delete_book(self):
"""
Ensure we can delete a book.
"""
url = reverse('book-detail', args=[self.book.id])
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Book.objects.count(), 0)

Advanced Testing Topics

Testing Authentication

Many APIs require authentication. Let's see how to test an API endpoint that requires authentication:

python
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token

class AuthenticatedAPITests(APITestCase):
def setUp(self):
# Create a user
self.user = User.objects.create_user(
username='testuser',
email='[email protected]',
password='testpass123'
)

# Create a token for the user
self.token = Token.objects.create(user=self.user)

# Add the token to the authorization header
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}')

# Create a test book
self.book = Book.objects.create(
title='Secret Book',
author='Secret Author',
published_date=datetime.date(2020, 1, 1),
isbn='1234567890123'
)

def test_authenticated_request(self):
"""
Ensure we can access protected endpoints when authenticated.
"""
url = reverse('book-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_unauthenticated_request(self):
"""
Ensure unauthenticated requests are rejected.
"""
# Remove the authentication credentials
self.client.credentials()

url = reverse('book-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

Testing Permissions

Testing different permission scenarios:

python
def test_admin_user_permissions(self):
"""
Ensure admin users can perform all actions.
"""
# Make the user a staff member
self.user.is_staff = True
self.user.save()

# Test creating a book
url = reverse('book-list')
data = {
'title': 'Admin Book',
'author': 'Admin Author',
'published_date': '2021-01-01',
'isbn': '9876543210123'
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_regular_user_permissions(self):
"""
Ensure regular users have limited permissions.
"""
# Assuming regular users can only read
url = reverse('book-list')
data = {
'title': 'Unauthorized Book',
'author': 'Unauthorized Author',
'published_date': '2021-01-01',
'isbn': '9876543210123'
}
response = self.client.post(url, data, format='json')
# Status code depends on your permission settings
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

Testing Serializers and Validation

Testing serializer validation logic:

python
from django.test import TestCase
from .serializers import BookSerializer

class BookSerializerTest(TestCase):
def test_valid_serializer(self):
data = {
'title': 'Test Book',
'author': 'Test Author',
'published_date': '2020-01-01',
'isbn': '1234567890123'
}
serializer = BookSerializer(data=data)
self.assertTrue(serializer.is_valid())

def test_invalid_serializer(self):
# Missing required field 'title'
data = {
'author': 'Test Author',
'published_date': '2020-01-01',
'isbn': '1234567890123'
}
serializer = BookSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn('title', serializer.errors)

Creating Test Fixtures

For complex tests, you might want to use fixtures to load test data:

  1. Create a fixture file books.json in a fixtures directory inside your app:
json
[
{
"model": "myapp.book",
"pk": 1,
"fields": {
"title": "Django for Beginners",
"author": "William S. Vincent",
"published_date": "2020-01-01",
"isbn": "1234567890123"
}
},
{
"model": "myapp.book",
"pk": 2,
"fields": {
"title": "Django for APIs",
"author": "William S. Vincent",
"published_date": "2020-02-01",
"isbn": "3210987654321"
}
}
]
  1. Load the fixtures in your test:
python
class BookFixtureTests(APITestCase):
fixtures = ['books.json']

def test_book_count(self):
self.assertEqual(Book.objects.count(), 2)

def test_get_books_from_fixture(self):
url = reverse('book-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 2)

Real-World Application: Testing an E-commerce API

Let's create a more complex test case for an e-commerce API:

python
class OrderAPITests(APITestCase):
def setUp(self):
# Create a user
self.user = User.objects.create_user(
username='customer',
email='[email protected]',
password='customerpass123'
)

# Authenticate the user
self.client.force_authenticate(user=self.user)

# Create a product
self.product = Product.objects.create(
name='Test Product',
description='Test description',
price=19.99,
stock=100
)

def test_create_order(self):
"""
Ensure a user can place an order.
"""
url = reverse('order-list')
data = {
'items': [
{'product_id': self.product.id, 'quantity': 2}
],
'shipping_address': {
'street': '123 Main St',
'city': 'Test City',
'state': 'TS',
'zip_code': '12345'
}
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

# Verify order details
order_id = response.data['id']
order_url = reverse('order-detail', args=[order_id])
order_response = self.client.get(order_url)

self.assertEqual(order_response.status_code, status.HTTP_200_OK)
self.assertEqual(len(order_response.data['items']), 1)
self.assertEqual(order_response.data['items'][0]['product_name'], 'Test Product')
self.assertEqual(order_response.data['items'][0]['quantity'], 2)
self.assertEqual(order_response.data['total'], 39.98)

# Check if stock was updated
product_url = reverse('product-detail', args=[self.product.id])
product_response = self.client.get(product_url)
self.assertEqual(product_response.data['stock'], 98)

Best Practices for API Testing

  1. Test isolation: Each test should be independent and not rely on the state from other tests.

  2. Use descriptive test names: Your test name should describe what the test is checking.

  3. Test both positive and negative cases: Don't just test that things work, but also test how your API handles errors.

  4. Use setUp and tearDown methods: To reuse code for test setup and cleanup.

  5. Test one thing per test: Each test should focus on testing a single behavior.

  6. Use appropriate assertions: Django REST Framework provides specific assertions for API testing.

  7. Test performance: For critical endpoints, consider adding tests that verify response times.

  8. Use test coverage tools: Use tools like coverage.py to ensure your tests are comprehensive:

bash
# Install coverage
pip install coverage

# Run tests with coverage
coverage run --source='.' manage.py test myapp

# Generate report
coverage report

Running Your Tests

To run your tests, use Django's test command:

bash
python manage.py test

To run a specific test case or method:

bash
python manage.py test myapp.tests.BookAPITests
python manage.py test myapp.tests.BookAPITests.test_create_book

Summary

We've covered the essentials of testing Django REST Framework APIs including:

  • Setting up test cases with APITestCase
  • Testing CRUD operations
  • Testing authentication and permissions
  • Validating serializers
  • Using fixtures for test data
  • Best practices for API testing

Testing your API ensures that your endpoints work as expected, validates your business logic, and provides confidence when making changes or adding new features. While it may seem like extra work up front, well-written tests save time in the long run and contribute to the overall quality of your API.

Additional Resources

  1. Django REST Framework Testing Documentation
  2. Django Testing Documentation
  3. Python unittest Documentation

Exercises

  1. Create a test case for an API endpoint that filters books by author.
  2. Write tests for API pagination to ensure it returns the correct number of items per page.
  3. Implement and test a custom permission class that only allows users to edit books they've created.
  4. Write tests for a search API that returns books based on title or author keywords.
  5. Create a test suite for an API that needs to handle file uploads for book covers.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)