Lesson 4
Testing Complex API in Django REST Framework
Introduction

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).

Recap of Setup: Data

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:

JSON
1[ 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]
Recap of Setup: Views

We will use a custom filter:

Python
1import 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:

Python
1from 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:

Python
1class 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
Testing Filtering Functionality

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:

Python
1from 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 from todos.json.
  • The setUp method initializes the URL for the Todo list endpoint.
  • We then have two test methods:
    • test_filter_completed_todos: This tests the filtering of completed Todo items.
    • test_filter_uncompleted_todos: This tests the filtering of uncompleted Todo 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, and group.
  • Combine multiple filter parameters in a single request.
  • Test the case when no items match the filter criteria.
Testing Sorting Functionality

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:

Python
1class 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 the priority 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 the priority values in the response are sorted as expected.

Suggestions for extending this test case:

  • Test sorting by other fields like task, completed, and assignee.
  • Combine multiple sorting fields in a single request.
Testing Pagination Functionality

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:

Python
1class 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 the page_size and page 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 the results matches the expected count.

Suggestions for extending this test case:

  • Test with different page_size values.
  • Test accessing a non-existing page.
Combining Operations in Test Cases

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:

Python
1from 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 by priority, 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 have completed set to False.
  • 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.
Explanation of urlencode

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:

Python
1from 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.

Overview and Summary

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!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.