In this lesson, we will explore testing complex API functionalities in the Django REST Framework, specifically filtering, sorting, and pagination. Testing these features ensures that our API behaves as expected when handling various user queries. By the end of this lesson, you will have the knowledge to write comprehensive test cases for these operations and combine them to test more complex scenarios effectively.
Note: In this lesson, we will consider only the basic setup for such tests. If you want to learn more about testing in the Django REST API and how to build stable tests that cover all the required behavior, check out the Advanced Testing for Django REST FRAMEWORKS app course path (coming soon).
Before we start with the new tests, let's briefly recap the setup of our Django project. In previous course, we covered filtering, sorting and pagination for your views. Here's a quick summary of the core components we'll use in this lesson:
We use a fixture stored in myapp/fixtures/todos.json
to ensure consistent test data:
JSON1[ 2 { 3 "model": "app.Todo", 4 "pk": 1, 5 "fields": { 6 "task": "Task 1", 7 "completed": false, 8 "priority": 1, 9 "assignee": "John", 10 "group": "Group A" 11 } 12 }, 13 { 14 "model": "app.Todo", 15 "pk": 2, 16 "fields": { 17 "task": "Task 2", 18 "completed": true, 19 "priority": 3, 20 "assignee": "Jane", 21 "group": "Group B" 22 } 23 }, 24 { 25 "model": "app.Todo", 26 "pk": 3, 27 "fields": { 28 "task": "Task 3", 29 "completed": false, 30 "priority": 2, 31 "assignee": "John", 32 "group": "Group A" 33 } 34 } 35]
We will use a custom filter:
Python1import django_filters 2from .models import Todo 3 4class TodoFilter(django_filters.FilterSet): 5 task = django_filters.CharFilter(field_name='task', lookup_expr='icontains') 6 priority = django_filters.NumberFilter(field_name='priority') 7 priority__gt = django_filters.NumberFilter(field_name='priority', lookup_expr='gt') 8 priority__lt = django_filters.NumberFilter(field_name='priority', lookup_expr='lt') 9 10 class Meta: 11 model = Todo 12 fields = ['task', 'completed', 'priority', 'assignee', 'group']
And a custom pagination:
Python1from rest_framework.pagination import PageNumberPagination 2 3class TodoPagination(PageNumberPagination): 4 page_size = 2 5 page_size_query_param = 'page_size' 6 max_page_size = 10
These classes are used in our ListCreateAPI
view to ensure it can handle sorting, filtering, and pagination:
Python1class TodoListCreate(generics.ListCreateAPIView): 2 queryset = Todo.objects.all() 3 serializer_class = TodoSerializer 4 filter_backends = [DjangoFilterBackend, OrderingFilter] 5 filterset_class = TodoFilter 6 ordering_fields = ['task', 'completed', 'priority', 'assignee', 'group'] 7 pagination_class = TodoPagination
Filtering allows users to retrieve specific subsets of data based on certain criteria. In our Todo
API, we'll start by testing the completed
field.
Let's write a test case to filter Todo
items by their completion status:
Python1from django.urls import reverse 2from rest_framework import status 3from rest_framework.test import APITestCase 4 5class FilterTodosTestCase(APITestCase): 6 fixtures = ['todos.json'] 7 8 def setUp(self): 9 self.list_url = reverse('todo-list') 10 11 def test_filter_completed_todos(self): 12 response = self.client.get(self.list_url, {'completed': True}) 13 self.assertEqual(response.status_code, status.HTTP_200_OK) 14 self.assertEqual(len(response.data['results']), 1) 15 self.assertTrue(all(todo['completed'] for todo in response.data['results'])) 16 17 def test_filter_uncompleted_todos(self): 18 response = self.client.get(self.list_url, {'completed': False}) 19 self.assertEqual(response.status_code, status.HTTP_200_OK) 20 self.assertEqual(len(response.data['results']), 2) 21 self.assertTrue(all(not todo['completed'] for todo in response.data['results']))
Reminder: As we have pagination enabled for the todos by default, in all test cases you must use response.data['results']
instead of response.data
. This ensures that you are accessing the actual list of todo items within the paginated response.
In the above test case:
- We use the
fixtures
attribute to load data fromtodos.json
. - The
setUp
method initializes the URL for theTodo
list endpoint. - We then have two test methods:
test_filter_completed_todos
: This tests the filtering of completedTodo
items.test_filter_uncompleted_todos
: This tests the filtering of uncompletedTodo
items.
- For each test, we make a GET request with the appropriate filter parameter and validate the response using assertions. We validate the number of returned items and that all items have a specified value for the field used for filtering.
Running these tests will ensure that our API correctly filters Todo
items based on their completion status.
Suggestions for extending this test case:
- Test filtering by other fields like
priority
,assignee
, andgroup
. - Combine multiple filter parameters in a single request.
- Test the case when no items match the filter criteria.
Sorting allows users to order data based on specific fields. We'll test sorting Todo
items by their priority
field.
Let's write a test case to sort Todo
items by their priority:
Python1class SortTodosTestCase(APITestCase): 2 fixtures = ['todos.json'] 3 4 def setUp(self): 5 self.list_url = reverse('todo-list') 6 7 def test_sort_todos_by_priority_asc(self): 8 response = self.client.get(self.list_url, {'ordering': 'priority'}) 9 self.assertEqual(response.status_code, status.HTTP_200_OK) 10 priorities = [todo['priority'] for todo in response.data['results']] 11 self.assertEqual(priorities, sorted(priorities)) 12 13 def test_sort_todos_by_priority_desc(self): 14 response = self.client.get(self.list_url, {'ordering': '-priority'}) 15 self.assertEqual(response.status_code, status.HTTP_200_OK) 16 priorities = [todo['priority'] for todo in response.data['results']] 17 self.assertEqual(priorities, sorted(priorities, reverse=True))
In this test case:
- We test the sorting functionality of the
Todo
list endpoint by thepriority
field in both ascending and descending order. - The
test_sort_todos_by_priority_asc
method validates the ascending sort order. - The
test_sort_todos_by_priority_desc
method validates the descending sort order. - Both methods check that the response status is
200 OK
and that thepriority
values in the response are sorted as expected.
Suggestions for extending this test case:
- Test sorting by other fields like
task
,completed
, andassignee
. - Combine multiple sorting fields in a single request.
Pagination allows users to retrieve data in chunks or pages. We'll test the pagination of Todo
items.
Let's write a test case to paginate Todo
items:
Python1class PaginateTodosTestCase(APITestCase): 2 fixtures = ['todos.json'] 3 4 def setUp(self): 5 self.list_url = reverse('todo-list') 6 7 def test_paginate_todos_first_page(self): 8 response = self.client.get(self.list_url, {'page_size': 2, 'page': 1}) 9 self.assertEqual(response.status_code, status.HTTP_200_OK) 10 self.assertEqual(len(response.data['results']), 2) 11 12 def test_paginate_todos_second_page(self): 13 response = self.client.get(self.list_url, {'page_size': 2, 'page': 2}) 14 self.assertEqual(response.status_code, status.HTTP_200_OK) 15 self.assertEqual(len(response.data['results']), 1)
In this test case:
- We test the pagination functionality of the
Todo
list endpoint by specifying thepage_size
andpage
parameters. - The
test_paginate_todos_first_page
method checks the first page of results, ensuring it contains 2 items. - The
test_paginate_todos_second_page
method checks the second page of results, ensuring it contains 1 item. - Both methods verify that the response status is
200 OK
and the number of items in theresults
matches the expected count.
Suggestions for extending this test case:
- Test with different
page_size
values. - Test accessing a non-existing page.
Combining filtering, sorting, and pagination allows for more complex queries. We'll test a combination of these operations.
Let's write a test case combining filtering, sorting, and pagination:
Python1from django.utils.http import urlencode 2 3class CombinedOperationsTestCase(APITestCase): 4 fixtures = ['todos.json'] 5 6 def setUp(self): 7 self.list_url = reverse('todo-list') 8 9 def test_combined_filter_sort_paginate(self): 10 params = urlencode({'completed': False, 'ordering': 'priority', 'page_size': 2, 'page': 1}) 11 response = self.client.get(f"{self.list_url}?{params}") 12 self.assertEqual(response.status_code, status.HTTP_200_OK) 13 self.assertEqual(len(response.data['results']), 2) 14 self.assertTrue(all(not todo['completed'] for todo in response.data['results'])) 15 priorities = [todo['priority'] for todo in response.data['results']] 16 self.assertEqual(priorities, sorted(priorities))
In this test case:
- We combine filtering by
completed
, sorting bypriority
, and pagination parameters. - The
assertTrue(all(not todo['completed'] for todo in response.data['results']))
line iterates over all items in the response data to check that they all havecompleted
set toFalse
. - The
priorities
list is created to hold all priority values from the response, ensuring they are in ascending order. This list is then compared to the sorted version of itself to confirm that the priorities are sorted correctly.
Suggestions for extending this test case:
- Combine different fields for filtering and sorting.
- Test combined operations with varying
page_size
values. - Test combinations with more complex filter criteria.
- Filter items in a way that results in 0 items returned.
urlencode
is a helpful utility from django.utils.http
that transforms a dictionary of parameters into a URL-encoded query string. This is particularly useful when constructing complex URLs with multiple query parameters.
Example:
Python1from django.utils.http import urlencode 2 3params = {'completed': False, 'ordering': 'priority', 'page_size': 2} 4query_string = urlencode(params) 5print(query_string) # Output: "completed=False&ordering=priority&page_size=2"
In the combined test case, using urlencode
ensures that the query parameters are correctly formatted for the GET request.
In this lesson, we covered:
- Testing filtering functionality in Django REST Framework.
- Testing sorting functionality.
- Testing pagination functionality.
- Combining filtering, sorting, and pagination operations in test cases.
Each section provided practical examples and explanations to help you understand how to write comprehensive test cases for these functionalities.
As you move on to the practice exercises, remember to apply the concepts learned here. These exercises will solidify your understanding and prepare you for real-world scenarios. Keep practicing to get comfortable with testing complex APIs in Django REST Framework.
Congratulations on completing this lesson! You're well on your way to mastering the art of testing Django APIs. Happy coding!