0
# Testing Utilities
1
2
Comprehensive testing tools including API client, request factory, and test case classes for thorough API testing in Django REST Framework.
3
4
## Capabilities
5
6
### API Request Factory
7
8
Factory for creating mock API requests for testing.
9
10
```python { .api }
11
class APIRequestFactory(DjangoRequestFactory):
12
"""
13
Factory for creating API requests without going through Django's WSGI interface.
14
"""
15
def __init__(self, enforce_csrf_checks=False, **defaults):
16
"""
17
Args:
18
enforce_csrf_checks (bool): Enable CSRF checking
19
**defaults: Default request parameters
20
"""
21
self.enforce_csrf_checks = enforce_csrf_checks
22
super().__init__(**defaults)
23
24
def _encode_data(self, data, content_type):
25
"""
26
Encode data for request body based on content type.
27
28
Args:
29
data: Data to encode
30
content_type (str): Content type for encoding
31
32
Returns:
33
tuple: (encoded_data, content_type)
34
"""
35
36
def get(self, path, data=None, **extra):
37
"""
38
Create GET request.
39
40
Args:
41
path (str): URL path
42
data (dict): Query parameters
43
**extra: Additional request parameters
44
45
Returns:
46
Request: Mock GET request
47
"""
48
49
def post(self, path, data=None, format=None, content_type=None, **extra):
50
"""
51
Create POST request.
52
53
Args:
54
path (str): URL path
55
data: Request body data
56
format (str): Data format ('json', 'multipart', etc.)
57
content_type (str): Custom content type
58
**extra: Additional request parameters
59
60
Returns:
61
Request: Mock POST request
62
"""
63
64
def put(self, path, data=None, format=None, content_type=None, **extra):
65
"""Create PUT request."""
66
67
def patch(self, path, data=None, format=None, content_type=None, **extra):
68
"""Create PATCH request."""
69
70
def delete(self, path, data=None, format=None, content_type=None, **extra):
71
"""Create DELETE request."""
72
73
def options(self, path, data=None, format=None, content_type=None, **extra):
74
"""Create OPTIONS request."""
75
76
def head(self, path, data=None, **extra):
77
"""Create HEAD request."""
78
79
def trace(self, path, **extra):
80
"""Create TRACE request."""
81
```
82
83
### API Test Client
84
85
Enhanced test client with API-specific functionality.
86
87
```python { .api }
88
class APIClient(APIRequestFactory, DjangoClient):
89
"""
90
Test client for making API requests in tests.
91
"""
92
def __init__(self, enforce_csrf_checks=False, **defaults):
93
"""
94
Args:
95
enforce_csrf_checks (bool): Enable CSRF checking
96
**defaults: Default client parameters
97
"""
98
super().__init__(enforce_csrf_checks=enforce_csrf_checks, **defaults)
99
self._credentials = {}
100
101
def credentials(self, **kwargs):
102
"""
103
Set authentication credentials for subsequent requests.
104
105
Args:
106
**kwargs: Authentication headers (HTTP_AUTHORIZATION, etc.)
107
"""
108
self._credentials = kwargs
109
110
def force_authenticate(self, user=None, token=None):
111
"""
112
Force authentication for subsequent requests.
113
114
Args:
115
user: User instance to authenticate as
116
token: Authentication token
117
"""
118
self.handler._force_user = user
119
self.handler._force_token = token
120
121
def logout(self):
122
"""
123
Remove authentication and credentials.
124
"""
125
self._credentials = {}
126
if hasattr(self.handler, '_force_user'):
127
del self.handler._force_user
128
if hasattr(self.handler, '_force_token'):
129
del self.handler._force_token
130
super().logout()
131
```
132
133
### Test Case Classes
134
135
Specialized test case classes for API testing.
136
137
```python { .api }
138
class APITestCase(testcases.TestCase):
139
"""
140
Test case class with APIClient and additional API testing utilities.
141
"""
142
client_class = APIClient
143
144
def setUp(self):
145
"""Set up test case with API client."""
146
super().setUp()
147
self.client = self.client_class()
148
149
def _pre_setup(self):
150
"""Pre-setup hook for test case."""
151
super()._pre_setup()
152
153
def _post_teardown(self):
154
"""Post-teardown hook for test case."""
155
super()._post_teardown()
156
157
class APITransactionTestCase(testcases.TransactionTestCase):
158
"""
159
Transaction test case for API testing with database transactions.
160
"""
161
client_class = APIClient
162
163
def _pre_setup(self):
164
"""Pre-setup with transaction handling."""
165
super()._pre_setup()
166
self.client = self.client_class()
167
168
class APISimpleTestCase(testcases.SimpleTestCase):
169
"""
170
Simple test case for API testing without database.
171
"""
172
client_class = APIClient
173
174
def setUp(self):
175
"""Set up simple test case."""
176
super().setUp()
177
self.client = self.client_class()
178
179
class APILiveServerTestCase(testcases.LiveServerTestCase):
180
"""
181
Live server test case for API integration testing.
182
"""
183
client_class = APIClient
184
185
def setUp(self):
186
"""Set up live server test case."""
187
super().setUp()
188
self.client = self.client_class()
189
190
class URLPatternsTestCase(testcases.SimpleTestCase):
191
"""
192
Test case for testing URL patterns and routing.
193
"""
194
def setUp(self):
195
"""Set up URL patterns test case."""
196
super().setUp()
197
self.client = APIClient()
198
```
199
200
### Test Utilities
201
202
Utility functions for API testing.
203
204
```python { .api }
205
def force_authenticate(request, user=None, token=None):
206
"""
207
Force authentication on a request object for testing.
208
209
Args:
210
request: Request object to authenticate
211
user: User instance to authenticate as
212
token: Authentication token
213
"""
214
request.user = user or AnonymousUser()
215
request.auth = token
216
```
217
218
## Usage Examples
219
220
### Basic API Testing
221
222
```python
223
from rest_framework.test import APITestCase, APIClient
224
from rest_framework import status
225
from django.contrib.auth.models import User
226
from myapp.models import Book
227
228
class BookAPITestCase(APITestCase):
229
def setUp(self):
230
"""Set up test data."""
231
self.user = User.objects.create_user(
232
username='testuser',
233
email='test@example.com',
234
password='testpass123'
235
)
236
self.book = Book.objects.create(
237
title='Test Book',
238
author='Test Author',
239
isbn='1234567890123'
240
)
241
242
def test_get_book_list(self):
243
"""Test retrieving book list."""
244
url = '/api/books/'
245
response = self.client.get(url)
246
247
self.assertEqual(response.status_code, status.HTTP_200_OK)
248
self.assertEqual(len(response.data), 1)
249
self.assertEqual(response.data[0]['title'], 'Test Book')
250
251
def test_create_book_authenticated(self):
252
"""Test creating book with authentication."""
253
self.client.force_authenticate(user=self.user)
254
255
url = '/api/books/'
256
data = {
257
'title': 'New Book',
258
'author': 'New Author',
259
'isbn': '9876543210987'
260
}
261
response = self.client.post(url, data, format='json')
262
263
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
264
self.assertEqual(Book.objects.count(), 2)
265
self.assertEqual(response.data['title'], 'New Book')
266
267
def test_create_book_unauthenticated(self):
268
"""Test creating book without authentication fails."""
269
url = '/api/books/'
270
data = {'title': 'Unauthorized Book'}
271
response = self.client.post(url, data, format='json')
272
273
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
274
```
275
276
### Token Authentication Testing
277
278
```python
279
from rest_framework.authtoken.models import Token
280
281
class TokenAuthTestCase(APITestCase):
282
def setUp(self):
283
self.user = User.objects.create_user(
284
username='testuser',
285
password='testpass123'
286
)
287
self.token = Token.objects.create(user=self.user)
288
289
def test_token_authentication(self):
290
"""Test API access with token authentication."""
291
# Set token in Authorization header
292
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key)
293
294
url = '/api/protected-endpoint/'
295
response = self.client.get(url)
296
297
self.assertEqual(response.status_code, status.HTTP_200_OK)
298
299
def test_invalid_token(self):
300
"""Test API access with invalid token."""
301
self.client.credentials(HTTP_AUTHORIZATION='Token invalid-token')
302
303
url = '/api/protected-endpoint/'
304
response = self.client.get(url)
305
306
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
307
308
def test_no_token(self):
309
"""Test API access without token."""
310
url = '/api/protected-endpoint/'
311
response = self.client.get(url)
312
313
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
314
```
315
316
### Testing Different Content Types
317
318
```python
319
class ContentTypeTestCase(APITestCase):
320
def test_json_request(self):
321
"""Test JSON content type."""
322
url = '/api/books/'
323
data = {'title': 'JSON Book', 'author': 'JSON Author'}
324
response = self.client.post(url, data, format='json')
325
326
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
327
328
def test_form_request(self):
329
"""Test form-encoded content type."""
330
url = '/api/books/'
331
data = {'title': 'Form Book', 'author': 'Form Author'}
332
response = self.client.post(url, data) # Default is form-encoded
333
334
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
335
336
def test_multipart_request(self):
337
"""Test multipart content type with file upload."""
338
url = '/api/books/'
339
with open('test_cover.jpg', 'rb') as cover_file:
340
data = {
341
'title': 'Book with Cover',
342
'author': 'Cover Author',
343
'cover_image': cover_file
344
}
345
response = self.client.post(url, data, format='multipart')
346
347
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
348
```
349
350
### Testing Custom Headers and Parameters
351
352
```python
353
class CustomHeaderTestCase(APITestCase):
354
def test_custom_header(self):
355
"""Test API with custom headers."""
356
url = '/api/books/'
357
response = self.client.get(
358
url,
359
HTTP_X_CUSTOM_HEADER='custom-value',
360
HTTP_ACCEPT='application/json'
361
)
362
363
self.assertEqual(response.status_code, status.HTTP_200_OK)
364
365
def test_query_parameters(self):
366
"""Test API with query parameters."""
367
url = '/api/books/'
368
response = self.client.get(url, {
369
'search': 'django',
370
'ordering': 'title',
371
'page': 1
372
})
373
374
self.assertEqual(response.status_code, status.HTTP_200_OK)
375
```
376
377
### Testing Pagination
378
379
```python
380
class PaginationTestCase(APITestCase):
381
def setUp(self):
382
# Create multiple books for pagination testing
383
for i in range(25):
384
Book.objects.create(
385
title=f'Book {i}',
386
author=f'Author {i}',
387
isbn=f'123456789012{i}'
388
)
389
390
def test_pagination_first_page(self):
391
"""Test first page of paginated results."""
392
url = '/api/books/'
393
response = self.client.get(url)
394
395
self.assertEqual(response.status_code, status.HTTP_200_OK)
396
self.assertIn('results', response.data)
397
self.assertIn('next', response.data)
398
self.assertIsNone(response.data['previous'])
399
self.assertEqual(len(response.data['results']), 20) # Default page size
400
401
def test_pagination_custom_page_size(self):
402
"""Test custom page size."""
403
url = '/api/books/?page_size=10'
404
response = self.client.get(url)
405
406
self.assertEqual(response.status_code, status.HTTP_200_OK)
407
self.assertEqual(len(response.data['results']), 10)
408
```
409
410
### Testing Error Handling
411
412
```python
413
class ErrorHandlingTestCase(APITestCase):
414
def test_validation_error(self):
415
"""Test validation error responses."""
416
url = '/api/books/'
417
data = {'title': ''} # Invalid: empty title
418
response = self.client.post(url, data, format='json')
419
420
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
421
self.assertIn('title', response.data)
422
423
def test_not_found_error(self):
424
"""Test 404 error for non-existent resource."""
425
url = '/api/books/999/' # Non-existent book ID
426
response = self.client.get(url)
427
428
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
429
430
def test_method_not_allowed(self):
431
"""Test 405 error for unsupported method."""
432
url = '/api/books/1/'
433
response = self.client.patch(url, {'title': 'Updated'}, format='json')
434
435
# Assuming PATCH is not allowed on this endpoint
436
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
437
```
438
439
### Unit Testing Views Directly
440
441
```python
442
from rest_framework.test import APIRequestFactory
443
from myapp.views import BookViewSet
444
445
class ViewUnitTestCase(APITestCase):
446
def setUp(self):
447
self.factory = APIRequestFactory()
448
self.user = User.objects.create_user(
449
username='testuser',
450
password='testpass123'
451
)
452
453
def test_viewset_list_action(self):
454
"""Test viewset list action directly."""
455
# Create request
456
request = self.factory.get('/api/books/')
457
request.user = self.user
458
459
# Test view directly
460
view = BookViewSet.as_view({'get': 'list'})
461
response = view(request)
462
463
self.assertEqual(response.status_code, status.HTTP_200_OK)
464
465
def test_viewset_create_action(self):
466
"""Test viewset create action directly."""
467
data = {'title': 'Direct Test Book', 'author': 'Test Author'}
468
request = self.factory.post('/api/books/', data, format='json')
469
request.user = self.user
470
471
view = BookViewSet.as_view({'post': 'create'})
472
response = view(request)
473
474
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
475
```
476
477
### Mocking External Services
478
479
```python
480
from unittest.mock import patch, Mock
481
482
class ExternalServiceTestCase(APITestCase):
483
@patch('myapp.services.external_api_call')
484
def test_with_mocked_service(self, mock_api_call):
485
"""Test API endpoint that calls external service."""
486
# Configure mock
487
mock_api_call.return_value = {'status': 'success', 'data': 'mocked'}
488
489
url = '/api/books/sync/'
490
response = self.client.post(url, format='json')
491
492
self.assertEqual(response.status_code, status.HTTP_200_OK)
493
mock_api_call.assert_called_once()
494
```