Skip to main content

Django: Testing APIs with Django REST Framework (DRF)

Django: Testing APIs with Django REST Framework (DRF)

Testing APIs built with Django REST Framework (DRF) ensures that endpoints function correctly, handle requests and responses as expected, and maintain reliability in Django’s Model-View-Template (MVT) architecture. DRF extends Django’s testing tools with utilities like APIClient and APITestCase to simulate API interactions. This guide covers best practices for testing DRF APIs, including setup, writing tests for CRUD operations, authentication, and error handling, assuming familiarity with Django, DRF, Python, and unit testing.


01. Why Test APIs with DRF?

APIs are critical for Django applications (e.g., mobile backends, web services, or microservices). Testing DRF APIs ensures:

  • Correct response data, status codes, and serialization.
  • Proper handling of authentication and permissions.
  • Robust error handling for invalid requests.
  • Prevention of regressions during development.

DRF’s testing tools integrate with Django’s test framework, providing a controlled environment for API validation.

Example: Basic API Test

# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from myapp.models import Product

class ProductAPITests(APITestCase):
    def setUp(self):
        self.product = Product.objects.create(name="Test Product", price=10.00)

    def test_get_product_list(self):
        response = self.client.get('/api/products/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 1)
        self.assertEqual(response.data[0]['name'], "Test Product")
python manage.py test

Output:

....
----------------------------------------------------------------------
Ran 1 test in 0.005s
OK

Explanation:

  • APITestCase - DRF’s test base class with a test database.
  • self.client - APIClient for simulating API requests.
  • Tests verify response status and data for a GET request.

02. Key DRF Testing Components

DRF’s testing framework builds on Django’s TestCase with API-specific tools. The table below summarizes key components:

Component Description Purpose
APITestCase Base class for API tests Manages test database and client
APIClient Simulates API requests Tests endpoints
status HTTP status codes Validates response codes
force_authenticate Bypasses authentication Tests authenticated endpoints
setUp Pre-test setup method Prepares test data


2.1 Setting Up the Test Environment

Create models, serializers, and views to test a simple product API.

Example: API Setup

# myapp/models.py
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.PositiveIntegerField()
# myapp/serializers.py
from rest_framework import serializers
from myapp.models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ['id', 'name', 'price', 'stock']
# myapp/views.py
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from myapp.models import Product
from myapp.serializers import ProductSerializer

class ProductListCreate(generics.ListCreateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

class ProductDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    permission_classes = [IsAuthenticated]
# myapp/urls.py
from django.urls import path
from myapp.views import ProductListCreate, ProductDetail

urlpatterns = [
    path('api/products/', ProductListCreate.as_view(), name='product_list_create'),
    path('api/products/<int:pk>/', ProductDetail.as_view(), name='product_detail'),
]

Explanation:

  • Defines a CRUD API for products with public list/create and authenticated detail endpoints.
  • ProductSerializer - Serializes model data for API responses.
  • Ready for testing GET, POST, PUT, DELETE, and authentication.

2.2 Testing GET Requests

Verify list and detail endpoints return correct data.

Example: GET Request Tests

# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from myapp.models import Product

class ProductAPITests(APITestCase):
    def setUp(self):
        self.product = Product.objects.create(name="Laptop", price=1000.00, stock=10)

    def test_get_product_list(self):
        url = reverse('product_list_create')
        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], {
            'id': self.product.id,
            'name': 'Laptop',
            'price': '1000.00',
            'stock': 10
        })

    def test_get_product_detail_unauthenticated(self):
        url = reverse('product_detail', kwargs={'pk': self.product.pk})
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

Output:

Ran 2 tests in 0.007s
OK

Explanation:

  • client.get - Simulates GET requests to list and detail endpoints.
  • Verifies status codes, response data structure, and unauthorized access.
  • reverse - Resolves URLs dynamically.

2.3 Testing POST Requests

Test creating new resources via POST requests.

Example: POST Request Test

# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from myapp.models import Product

class ProductAPITests(APITestCase):
    def test_create_product(self):
        url = reverse('product_list_create')
        data = {'name': 'Mouse', 'price': 25.00, 'stock': 100}
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Product.objects.count(), 1)
        self.assertEqual(response.data, {
            'id': Product.objects.first().id,
            'name': 'Mouse',
            'price': '25.00',
            'stock': 100
        })

    def test_create_invalid_product(self):
        url = reverse('product_list_create')
        data = {'name': '', 'price': -10.00, 'stock': -5}  # Invalid data
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertIn('name', response.data)
        self.assertIn('price', response.data)
        self.assertIn('stock', response.data)

Output:

Ran 2 tests in 0.008s
OK

Explanation:

  • client.post - Simulates POST requests with JSON data.
  • Tests successful creation and validation errors for invalid data.
  • format='json' - Ensures correct content type for API requests.

2.4 Testing Authentication and Permissions

Verify authenticated access and permission enforcement.

Example: Authentication Test

# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from django.contrib.auth.models import User
from myapp.models import Product

class ProductAPITests(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='testuser', password='testpass')
        self.product = Product.objects.create(name="Tablet", price=300.00, stock=5)

    def test_get_product_detail_authenticated(self):
        self.client.login(username='testuser', password='testpass')
        url = reverse('product_detail', kwargs={'pk': self.product.pk})
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['name'], 'Tablet')

    def test_get_product_detail_force_authenticate(self):
        self.client.force_authenticate(user=self.user)
        url = reverse('product_detail', kwargs={'pk': self.product.pk})
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['stock'], 5)

Output:

Ran 2 tests in 0.009s
OK

Explanation:

  • client.login - Simulates user login for session-based auth.
  • force_authenticate - Bypasses login for token-based or quick auth tests.
  • Verifies authenticated access to protected endpoints.

2.5 Testing PUT and DELETE Requests

Test updating and deleting resources.

Example: PUT and DELETE Tests

# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from django.contrib.auth.models import User
from myapp.models import Product

class ProductAPITests(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='testuser', password='testpass')
        self.client.force_authenticate(user=self.user)
        self.product = Product.objects.create(name="Keyboard", price=75.00, stock=20)

    def test_update_product(self):
        url = reverse('product_detail', kwargs={'pk': self.product.pk})
        data = {'name': 'Updated Keyboard', 'price': 80.00, 'stock': 15}
        response = self.client.put(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.product.refresh_from_db()
        self.assertEqual(self.product.name, 'Updated Keyboard')
        self.assertEqual(self.product.price, 80.00)
        self.assertEqual(self.product.stock, 15)

    def test_delete_product(self):
        url = reverse('product_detail', kwargs={'pk': self.product.pk})
        response = self.client.delete(url)
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
        self.assertFalse(Product.objects.filter(pk=self.product.pk).exists())

Output:

Ran 2 tests in 0.010s
OK

Explanation:

  • client.put - Tests updating resource fields.
  • client.delete - Tests resource deletion.
  • refresh_from_db - Ensures updated data is fetched from the database.

2.6 Mocking External Services

Mock external API calls to isolate tests.

Example: Mocking External API

pip install unittest-mock
# myapp/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
import requests

class StockCheck(APIView):
    def get(self, request):
        response = requests.get('https://api.example.com/stock')
        return Response(response.json())
# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from unittest.mock import patch

class StockCheckTests(APITestCase):
    @patch('requests.get')
    def test_stock_check(self, mock_get):
        mock_get.return_value.json.return_value = {'item': 'Laptop', 'stock': 50}
        url = reverse('stock_check')
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data, {'item': 'Laptop', 'stock': 50})

Output:

Ran 1 test in 0.004s
OK

Explanation:

  • patch - Mocks requests.get to avoid external calls.
  • Ensures tests are fast and independent of external services.

2.7 Incorrect Testing Approach

Example: Testing Without Format (Incorrect)

# myapp/tests.py (Incorrect)
from rest_framework.test import APITestCase
from django.urls import reverse

class ProductAPITests(APITestCase):
    def test_create_product(self):
        url = reverse('product_list_create')
        data = {'name': 'Mouse', 'price': 25.00, 'stock': 100}
        response = self.client.post(url, data)  # Missing format='json'
        self.assertEqual(response.status_code, 201)  # Unreliable

Output:

AssertionError: 400 != 201

Explanation:

  • Missing format='json' sends form-encoded data, causing validation errors.
  • Solution: Always specify format='json' for JSON-based APIs.

03. Effective Usage

3.1 Recommended Practices

  • Test edge cases and error conditions.

Example: Edge Case Testing

# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from myapp.models import Product

class ProductAPITests(APITestCase):
    def setUp(self):
        self.product = Product.objects.create(name="Monitor", price=200.00, stock=0)

    def test_get_nonexistent_product(self):
        url = reverse('product_detail', kwargs={'pk': 999})
        self.client.force_authenticate(user=User.objects.create_user('test', 'test'))
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

    def test_update_zero_stock(self):
        url = reverse('product_detail', kwargs={'pk': self.product.pk})
        self.client.force_authenticate(user=User.objects.create_user('test', 'test'))
        data = {'name': 'Monitor', 'price': 200.00, 'stock': 0}
        response = self.client.put(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data['stock'], 0)

Output:

Ran 2 tests in 0.006s
OK
  • Tests nonexistent resources and edge cases like zero stock.
  • Use descriptive test names for clarity.
  • Run tests with python manage.py test --parallel for faster execution.

3.2 Practices to Avoid

  • Avoid testing serializer implementation details.

Example: Testing Serializer Internals (Incorrect)

# myapp/tests.py (Incorrect)
from rest_framework.test import APITestCase
from myapp.serializers import ProductSerializer

class ProductAPITests(APITestCase):
    def test_serializer_fields(self):
        serializer = ProductSerializer()
        self.assertEqual(serializer.fields.keys(), {'id', 'name', 'price', 'stock'})

Output:

Ran 1 test in 0.003s
OK
  • Testing serializer fields ties tests to implementation, breaking on refactors.
  • Solution: Test API responses to verify serialization behavior.

04. Common Use Cases

4.1 Testing Token Authentication

Verify APIs secured with token-based authentication.

Example: Token Authentication Test

# myapp/views.py
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from myapp.models import Product
from myapp.serializers import ProductSerializer

class ProductList(generics.ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    permission_classes = [IsAuthenticated]
# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
from myapp.models import Product

class ProductTokenTests(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='testuser', password='testpass')
        self.token = Token.objects.create(user=self.user)
        self.product = Product.objects.create(name="Speaker", price=100.00, stock=30)

    def test_token_authentication(self):
        self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}')
        url = reverse('product_list')
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 1)

    def test_no_token(self):
        url = reverse('product_list')
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

Output:

Ran 2 tests in 0.008s
OK

Explanation:

  • client.credentials - Sets token authentication header.
  • Tests both authenticated and unauthenticated requests.

4.2 Testing Pagination

Verify paginated API responses.

Example: Pagination Test

# myapp/views.py
from rest_framework import generics
from rest_framework.pagination import PageNumberPagination
from myapp.models import Product
from myapp.serializers import ProductSerializer

class ProductList(generics.ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = PageNumberPagination
# myapp/tests.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from myapp.models import Product

class ProductPaginationTests(APITestCase):
    def setUp(self):
        for i in range(15):
            Product.objects.create(name=f"Product {i}", price=50.00, stock=10)

    def test_pagination(self):
        url = reverse('product_list') + '?page=1'
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 10)  # Default page size
        self.assertIn('next', response.data)
        self.assertIn('previous', response.data)

Output:

Ran 1 test in 0.012s
OK

Explanation:

  • Tests pagination metadata and page size.
  • Verifies paginated response structure.

Conclusion

Testing DRF APIs ensures robust and reliable endpoints. Key takeaways:

  • Use APITestCase and APIClient for API-specific testing.
  • Test CRUD operations, authentication, and error cases.
  • Mock external dependencies for isolated tests.
  • Verify pagination and edge cases for complete coverage.

With thorough API tests, you can build dependable DRF-based applications! For more details, refer to the DRF testing documentation and Django testing documentation.

Comments