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
- Mocksrequests.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
andAPIClient
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
Post a Comment