Web APIs for Django, made easy.
—
Comprehensive testing tools including API client, request factory, and test case classes for thorough API testing in Django REST Framework.
Factory for creating mock API requests for testing.
class APIRequestFactory(DjangoRequestFactory):
"""
Factory for creating API requests without going through Django's WSGI interface.
"""
def __init__(self, enforce_csrf_checks=False, **defaults):
"""
Args:
enforce_csrf_checks (bool): Enable CSRF checking
**defaults: Default request parameters
"""
self.enforce_csrf_checks = enforce_csrf_checks
super().__init__(**defaults)
def _encode_data(self, data, content_type):
"""
Encode data for request body based on content type.
Args:
data: Data to encode
content_type (str): Content type for encoding
Returns:
tuple: (encoded_data, content_type)
"""
def get(self, path, data=None, **extra):
"""
Create GET request.
Args:
path (str): URL path
data (dict): Query parameters
**extra: Additional request parameters
Returns:
Request: Mock GET request
"""
def post(self, path, data=None, format=None, content_type=None, **extra):
"""
Create POST request.
Args:
path (str): URL path
data: Request body data
format (str): Data format ('json', 'multipart', etc.)
content_type (str): Custom content type
**extra: Additional request parameters
Returns:
Request: Mock POST request
"""
def put(self, path, data=None, format=None, content_type=None, **extra):
"""Create PUT request."""
def patch(self, path, data=None, format=None, content_type=None, **extra):
"""Create PATCH request."""
def delete(self, path, data=None, format=None, content_type=None, **extra):
"""Create DELETE request."""
def options(self, path, data=None, format=None, content_type=None, **extra):
"""Create OPTIONS request."""
def head(self, path, data=None, **extra):
"""Create HEAD request."""
def trace(self, path, **extra):
"""Create TRACE request."""Enhanced test client with API-specific functionality.
class APIClient(APIRequestFactory, DjangoClient):
"""
Test client for making API requests in tests.
"""
def __init__(self, enforce_csrf_checks=False, **defaults):
"""
Args:
enforce_csrf_checks (bool): Enable CSRF checking
**defaults: Default client parameters
"""
super().__init__(enforce_csrf_checks=enforce_csrf_checks, **defaults)
self._credentials = {}
def credentials(self, **kwargs):
"""
Set authentication credentials for subsequent requests.
Args:
**kwargs: Authentication headers (HTTP_AUTHORIZATION, etc.)
"""
self._credentials = kwargs
def force_authenticate(self, user=None, token=None):
"""
Force authentication for subsequent requests.
Args:
user: User instance to authenticate as
token: Authentication token
"""
self.handler._force_user = user
self.handler._force_token = token
def logout(self):
"""
Remove authentication and credentials.
"""
self._credentials = {}
if hasattr(self.handler, '_force_user'):
del self.handler._force_user
if hasattr(self.handler, '_force_token'):
del self.handler._force_token
super().logout()Specialized test case classes for API testing.
class APITestCase(testcases.TestCase):
"""
Test case class with APIClient and additional API testing utilities.
"""
client_class = APIClient
def setUp(self):
"""Set up test case with API client."""
super().setUp()
self.client = self.client_class()
def _pre_setup(self):
"""Pre-setup hook for test case."""
super()._pre_setup()
def _post_teardown(self):
"""Post-teardown hook for test case."""
super()._post_teardown()
class APITransactionTestCase(testcases.TransactionTestCase):
"""
Transaction test case for API testing with database transactions.
"""
client_class = APIClient
def _pre_setup(self):
"""Pre-setup with transaction handling."""
super()._pre_setup()
self.client = self.client_class()
class APISimpleTestCase(testcases.SimpleTestCase):
"""
Simple test case for API testing without database.
"""
client_class = APIClient
def setUp(self):
"""Set up simple test case."""
super().setUp()
self.client = self.client_class()
class APILiveServerTestCase(testcases.LiveServerTestCase):
"""
Live server test case for API integration testing.
"""
client_class = APIClient
def setUp(self):
"""Set up live server test case."""
super().setUp()
self.client = self.client_class()
class URLPatternsTestCase(testcases.SimpleTestCase):
"""
Test case for testing URL patterns and routing.
"""
def setUp(self):
"""Set up URL patterns test case."""
super().setUp()
self.client = APIClient()Utility functions for API testing.
def force_authenticate(request, user=None, token=None):
"""
Force authentication on a request object for testing.
Args:
request: Request object to authenticate
user: User instance to authenticate as
token: Authentication token
"""
request.user = user or AnonymousUser()
request.auth = tokenfrom rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth.models import User
from myapp.models import Book
class BookAPITestCase(APITestCase):
def setUp(self):
"""Set up test data."""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.book = Book.objects.create(
title='Test Book',
author='Test Author',
isbn='1234567890123'
)
def test_get_book_list(self):
"""Test retrieving book list."""
url = '/api/books/'
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')
def test_create_book_authenticated(self):
"""Test creating book with authentication."""
self.client.force_authenticate(user=self.user)
url = '/api/books/'
data = {
'title': 'New Book',
'author': 'New Author',
'isbn': '9876543210987'
}
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(response.data['title'], 'New Book')
def test_create_book_unauthenticated(self):
"""Test creating book without authentication fails."""
url = '/api/books/'
data = {'title': 'Unauthorized Book'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)from rest_framework.authtoken.models import Token
class TokenAuthTestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.token = Token.objects.create(user=self.user)
def test_token_authentication(self):
"""Test API access with token authentication."""
# Set token in Authorization header
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
url = '/api/protected-endpoint/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_invalid_token(self):
"""Test API access with invalid token."""
self.client.credentials(HTTP_AUTHORIZATION='Token invalid-token')
url = '/api/protected-endpoint/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_no_token(self):
"""Test API access without token."""
url = '/api/protected-endpoint/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)class ContentTypeTestCase(APITestCase):
def test_json_request(self):
"""Test JSON content type."""
url = '/api/books/'
data = {'title': 'JSON Book', 'author': 'JSON Author'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_form_request(self):
"""Test form-encoded content type."""
url = '/api/books/'
data = {'title': 'Form Book', 'author': 'Form Author'}
response = self.client.post(url, data) # Default is form-encoded
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_multipart_request(self):
"""Test multipart content type with file upload."""
url = '/api/books/'
with open('test_cover.jpg', 'rb') as cover_file:
data = {
'title': 'Book with Cover',
'author': 'Cover Author',
'cover_image': cover_file
}
response = self.client.post(url, data, format='multipart')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)class CustomHeaderTestCase(APITestCase):
def test_custom_header(self):
"""Test API with custom headers."""
url = '/api/books/'
response = self.client.get(
url,
HTTP_X_CUSTOM_HEADER='custom-value',
HTTP_ACCEPT='application/json'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_query_parameters(self):
"""Test API with query parameters."""
url = '/api/books/'
response = self.client.get(url, {
'search': 'django',
'ordering': 'title',
'page': 1
})
self.assertEqual(response.status_code, status.HTTP_200_OK)class PaginationTestCase(APITestCase):
def setUp(self):
# Create multiple books for pagination testing
for i in range(25):
Book.objects.create(
title=f'Book {i}',
author=f'Author {i}',
isbn=f'123456789012{i}'
)
def test_pagination_first_page(self):
"""Test first page of paginated results."""
url = '/api/books/'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('results', response.data)
self.assertIn('next', response.data)
self.assertIsNone(response.data['previous'])
self.assertEqual(len(response.data['results']), 20) # Default page size
def test_pagination_custom_page_size(self):
"""Test custom page size."""
url = '/api/books/?page_size=10'
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 10)class ErrorHandlingTestCase(APITestCase):
def test_validation_error(self):
"""Test validation error responses."""
url = '/api/books/'
data = {'title': ''} # Invalid: empty title
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('title', response.data)
def test_not_found_error(self):
"""Test 404 error for non-existent resource."""
url = '/api/books/999/' # Non-existent book ID
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_method_not_allowed(self):
"""Test 405 error for unsupported method."""
url = '/api/books/1/'
response = self.client.patch(url, {'title': 'Updated'}, format='json')
# Assuming PATCH is not allowed on this endpoint
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)from rest_framework.test import APIRequestFactory
from myapp.views import BookViewSet
class ViewUnitTestCase(APITestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
def test_viewset_list_action(self):
"""Test viewset list action directly."""
# Create request
request = self.factory.get('/api/books/')
request.user = self.user
# Test view directly
view = BookViewSet.as_view({'get': 'list'})
response = view(request)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_viewset_create_action(self):
"""Test viewset create action directly."""
data = {'title': 'Direct Test Book', 'author': 'Test Author'}
request = self.factory.post('/api/books/', data, format='json')
request.user = self.user
view = BookViewSet.as_view({'post': 'create'})
response = view(request)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)from unittest.mock import patch, Mock
class ExternalServiceTestCase(APITestCase):
@patch('myapp.services.external_api_call')
def test_with_mocked_service(self, mock_api_call):
"""Test API endpoint that calls external service."""
# Configure mock
mock_api_call.return_value = {'status': 'success', 'data': 'mocked'}
url = '/api/books/sync/'
response = self.client.post(url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
mock_api_call.assert_called_once()Install with Tessl CLI
npx tessl i tessl/pypi-djangorestframework