Django REST API – Testing APIs
In this post, we will add automated tests to our Django REST API.
Testing helps us make sure that authentication, permissions, ownership rules, and validation continue to work as expected.
Why testing APIs matters
As an API grows, testing everything manually becomes slow and error-prone.
With automated tests, we can check the most important API behaviours with one command:
python manage.py test
For this introductory post, we will focus on the essentials:
Authentication
Project listing
Project creation
Ownership rules
Validation errors
Task creation permissions
Creating the tests file
Django already created a tests.py file inside the projects app.
projects/
models.py
serializers.py
permissions.py
views.py
urls.py
tests.py
Open config/projects/tests.py and add the imports:
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Project, Task
We will use APITestCase to send requests to our API and check the response status codes and returned data.
Testing project endpoints
Let’s start by testing the most important project behaviours.
class ProjectAPITests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='john',
password='testpass123'
)
self.other_user = User.objects.create_user(
username='mary',
password='testpass123'
)
self.project = Project.objects.create(
owner=self.user,
name='Website Project',
description='Build a new website'
)
The setUp method runs before each test. Here we create two users and one sample project.
Testing authentication
Anonymous users should not be able to access protected endpoints.
def test_unauthenticated_user_cannot_access_projects(self):
response = self.client.get('/api/projects/')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
With the authentication setup used in this series, an unauthenticated request returns 403 Forbidden.
Testing project listing
To test authenticated requests, we can force the test client to act as a specific user.
def test_authenticated_user_can_list_projects(self):
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/projects/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['name'], 'Website Project')
Because we added pagination earlier, the API response contains count and results.
{
"count": 1,
"next": null,
"previous": null,
"results": [...]
}
Testing ownership
Users should only see their own projects.
def test_user_only_sees_own_projects(self):
Project.objects.create(
owner=self.other_user,
name='Other User Project',
description='This project belongs to another user'
)
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/projects/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['name'], 'Website Project')
Even though there are two projects in the database, the API only returns the project owned by the authenticated user.
Testing project creation
An authenticated user should be able to create a project.
def test_authenticated_user_can_create_project(self):
self.client.force_authenticate(user=self.user)
data = {
'name': 'Mobile App',
'description': 'Create a mobile application'
}
response = self.client.post('/api/projects/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Project.objects.count(), 2)
self.assertEqual(Project.objects.last().owner, self.user)
This checks that the API creates the project and automatically assigns the authenticated user as the owner.
Testing validation
In the previous post, we added validation to prevent very short project names. We can test that rule too.
def test_project_name_must_have_at_least_three_characters(self):
self.client.force_authenticate(user=self.user)
data = {
'name': 'AI',
'description': 'Too short'
}
response = self.client.post('/api/projects/', data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('name', response.data)
The API should reject the request and return an error for the name field.
Complete project tests
The project tests now look like this:
class ProjectAPITests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='john',
password='testpass123'
)
self.other_user = User.objects.create_user(
username='mary',
password='testpass123'
)
self.project = Project.objects.create(
owner=self.user,
name='Website Project',
description='Build a new website'
)
def test_unauthenticated_user_cannot_access_projects(self):
response = self.client.get('/api/projects/')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_authenticated_user_can_list_projects(self):
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/projects/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['name'], 'Website Project')
def test_user_only_sees_own_projects(self):
Project.objects.create(
owner=self.other_user,
name='Other User Project',
description='This project belongs to another user'
)
self.client.force_authenticate(user=self.user)
response = self.client.get('/api/projects/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['name'], 'Website Project')
def test_authenticated_user_can_create_project(self):
self.client.force_authenticate(user=self.user)
data = {
'name': 'Mobile App',
'description': 'Create a mobile application'
}
response = self.client.post('/api/projects/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Project.objects.count(), 2)
self.assertEqual(Project.objects.last().owner, self.user)
def test_project_name_must_have_at_least_three_characters(self):
self.client.force_authenticate(user=self.user)
data = {
'name': 'AI',
'description': 'Too short'
}
response = self.client.post('/api/projects/', data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('name', response.data)
This is enough to cover the most important project API behaviour without making the post too heavy.
Testing task permissions
Now let’s add a small task test class. The main thing we want to confirm is that users can only create tasks inside their own projects.
class TaskAPITests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='john',
password='testpass123'
)
self.other_user = User.objects.create_user(
username='mary',
password='testpass123'
)
self.project = Project.objects.create(
owner=self.user,
name='Website Project',
description='Build a new website'
)
self.other_project = Project.objects.create(
owner=self.other_user,
name='Other Project',
description='Another user project'
)
def test_user_can_create_task_in_own_project(self):
self.client.force_authenticate(user=self.user)
data = {
'project': self.project.id,
'title': 'Create contact page',
'description': 'Add a contact form',
'completed': False
}
response = self.client.post('/api/tasks/', data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Task.objects.count(), 1)
def test_user_cannot_create_task_in_other_user_project(self):
self.client.force_authenticate(user=self.user)
data = {
'project': self.other_project.id,
'title': 'Invalid task',
'description': 'This should not be allowed',
'completed': False
}
response = self.client.post('/api/tasks/', data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(Task.objects.count(), 0)
These two tests are enough for an introductory example. They show the difference between creating a task in the user’s own project and trying to create one in another user’s project.
Running the tests
Run the test suite with:
python manage.py test
If everything is working correctly, you should see an output similar to this:
Ran 7 tests in 1.842s
OK
If a test fails, check the status code, the request data, authentication, permissions, and validation rules.
During development, you can temporarily print the response data:
print(response.data)
What we have learned
In this post, we added automated tests to our Django REST API.
APITestCase is useful for API tests
force_authenticate allows authenticated requests
Tests can check status codes and response data
Ownership rules should be tested carefully
Validation errors should be tested
python manage.py test runs the test suite
We did not test every possible endpoint in this post. Instead, we focused on the most important behaviours and kept the examples simple.
What comes next?
In the next post, we will generate API documentation.
We will use OpenAPI and Swagger to create interactive documentation for our Django REST API endpoints.
Django REST API: API Documentation with Swagger/OpenAPI