How to Test a Django REST Framework API

Learn how to test a Django REST Framework API with APITestCase and APIClient, plus real Python examples, authentication tests, and live endpoint checks.

INEZA Felin-Michel

INEZA Felin-Michel

22 May 2026

How to Test a Django REST Framework API

Apidog for Enterprise

On-Premises Deploy

SSO & RBAC

SOC 2 Compliant

Explore Apidog Enterprise

Django REST Framework, usually shortened to DRF, is the standard toolkit for building APIs on top of Django. It gives you serializers, viewsets, routers, and an authentication layer. What it also gives you, and what many developers underuse, is a solid testing layer that builds on Django’s own test runner.

This guide shows how to test a DRF API two ways. First, automated tests written in Python using DRF’s APITestCase and APIClient, which run without a live server and catch regressions on every commit. Second, exercising the running endpoints with an API client, which is how you explore behavior and verify the real service. Both matter, and they catch different problems.

Set up the project and test environment

Start with an isolated environment so test dependencies stay separate from the rest of your system:

python -m venv venv
source venv/bin/activate
pip install django djangorestframework coverage

A DRF project tests like any Django project. By convention, tests live in a tests.py file inside each app, or in a tests/ package if you have many. Django’s test runner discovers any class that subclasses a TestCase variant and any method whose name starts with test_.

The key choice is which base class to use. Plain unittest.TestCase has no database. Django’s TestCase wraps each test in a transaction and rolls it back, so tests do not pollute each other. DRF’s APITestCase extends Django’s TestCase and swaps in DRF’s APIClient, which understands DRF authentication and content types. For API work, use APITestCase.

One more class is worth knowing. TransactionTestCase does not wrap tests in a transaction, which you need when the code under test manages transactions itself or relies on database features that a wrapping transaction would hide. It is slower because it truncates tables between tests instead of rolling back, so reach for it only when TestCase genuinely cannot model the behavior. For the large majority of endpoint tests, APITestCase is the right and fastest choice.

It also helps to think about test data early. Use a setUp method to create the rows each test in a class needs, or setUpTestData if the data is read-only and can be shared across the class for speed. For larger projects, a factory library such as factory_boy generates valid model instances without you spelling out every field, which keeps tests short and resilient when a model gains a new required field.

Test serializers as units

Serializers do validation and convert between model instances and JSON. They are small and pure, which makes them ideal for fast unit tests that do not touch endpoints.

Suppose you have an Article model and an ArticleSerializer. A unit test checks that valid data passes and invalid data fails:

from django.test import TestCase
from articles.serializers import ArticleSerializer


class ArticleSerializerTests(TestCase):

    def test_valid_data_passes(self):
        data = {"title": "Caching strategies", "body": "Use ETags."}
        serializer = ArticleSerializer(data=data)
        self.assertTrue(serializer.is_valid())

    def test_missing_title_fails(self):
        data = {"body": "No title here."}
        serializer = ArticleSerializer(data=data)
        self.assertFalse(serializer.is_valid())
        self.assertIn("title", serializer.errors)

These tests run in milliseconds because there is no HTTP and no view. They tell you the validation rules are correct before you even consider the endpoint. This unit-level coverage is the base of the testing pyramid described in what automated testing is.

Test endpoints with APITestCase and APIClient

Endpoint tests check the full path: routing, the view, the serializer, and the response. DRF’s APIClient sends requests in-process, so no server runs and tests stay fast.

from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from articles.models import Article


class ArticleEndpointTests(APITestCase):

    def setUp(self):
        Article.objects.create(title="First post", body="Hello world")

    def test_list_articles_returns_200(self):
        url = reverse("article-list")
        response = self.client.get(url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 1)

    def test_create_article(self):
        url = reverse("article-list")
        payload = {"title": "Second post", "body": "More content"}
        response = self.client.post(url, payload, format="json")
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Article.objects.count(), 2)

A few details worth noting. self.client is an APIClient instance provided by APITestCase. Use reverse() with the route name instead of hardcoding URLs, so a route change does not break every test. Pass format="json" on writes so the client serializes the payload correctly. Assert on the status code with the named constants from rest_framework.status, since they read more clearly than raw numbers. The status codes themselves are covered in the guide on HTTP status codes REST APIs should use.

Test authentication and permissions

Most real APIs protect their endpoints. You need tests proving that unauthenticated requests are rejected and authenticated requests with the right permissions succeed.

APIClient offers two ways to authenticate a test. force_authenticate() skips the credential check and attaches a user directly, which is ideal for testing the view logic itself. login() or credentials() exercise the real authentication path. Here is a permission test using both directions:

from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from django.urls import reverse


class ArticlePermissionTests(APITestCase):

    def setUp(self):
        self.user = User.objects.create_user(
            username="editor", password="testpass123"
        )
        self.url = reverse("article-list")

    def test_anonymous_cannot_create(self):
        payload = {"title": "Blocked", "body": "Should fail"}
        response = self.client.post(self.url, payload, format="json")
        self.assertEqual(
            response.status_code, status.HTTP_403_FORBIDDEN
        )

    def test_authenticated_user_can_create(self):
        self.client.force_authenticate(user=self.user)
        payload = {"title": "Allowed", "body": "Should pass"}
        response = self.client.post(self.url, payload, format="json")
        self.assertEqual(
            response.status_code, status.HTTP_201_CREATED
        )

The first test confirms the permission class blocks anonymous writes. The second confirms an authenticated user gets through. Together they pin down the permission boundary, which is exactly the kind of behavior that quietly breaks during refactors. If you are testing token auth specifically, swap force_authenticate for self.client.credentials(HTTP_AUTHORIZATION="Token " + token) to exercise the real header path.

It is worth being deliberate about which to use. force_authenticate is faster and isolates the view logic, so it suits the bulk of permission tests where you only care that a role can or cannot do something. The real credential path is what you want for a smaller set of tests that specifically prove the auth mechanism works: that a bad token is rejected, that an expired token fails, that the login endpoint issues a usable token. Mixing both gives you broad coverage cheaply and deep coverage where it counts.

Do not forget object-level permissions. Many DRF APIs allow a user to edit their own records but not someone else’s. Test that explicitly: create two users, have one create a record, then assert the other user gets a 403 or 404 when trying to modify it. List endpoints deserve the same scrutiny, since a leaky queryset can return rows a user should never see even when the detail endpoint is locked down.

Run the suite and measure coverage

Run every test with Django’s runner:

python manage.py test

The runner discovers your test classes, sets up a throwaway test database, runs each test inside a transaction, and rolls back after. A clean run prints OK. A failure shows the assertion that broke and where.

Knowing the suite passes is not the same as knowing it covers enough. The coverage tool measures which lines actually ran:

coverage run --source='.' manage.py test
coverage report
coverage html

The report lists each file with the percentage of lines exercised. The HTML output highlights untested lines in red, which points you straight at gaps. Aim for meaningful coverage of views, serializers, and permissions rather than a single headline number. The piece on how to write automated test scripts covers what makes a test worth keeping, since coverage of weak tests is not real safety.

Test the live API with a client

Automated Python tests are fast and run in CI, but they exercise the app in-process. They do not catch problems that only appear against the running service: a misconfigured CORS header, a reverse proxy stripping something, a slow database under real load, or a difference between your test database and production. For that you send real requests to the deployed endpoints.

Apidog is a strong fit here. It is an all-in-one API platform, so you can import your DRF API’s OpenAPI schema, send live requests to a running server, and build assertions visually without writing more Python. DRF can generate an OpenAPI schema, and Apidog consumes it directly, which keeps your client in sync with the actual contract. You can also build multi-step test scenarios, for example log in, create an article, fetch it, delete it, and run them on a schedule or in CI. This complements your APITestCase suite rather than replacing it: unit and endpoint tests guard the code, the API client guards the deployed service. You can download Apidog to import a DRF schema and try it. For teams that prefer staying in Python end to end, the pytest API automated testing framework guide shows how to run DRF-style tests under pytest.

Frequently asked questions

What is the difference between TestCase and APITestCase?

Django’s TestCase wraps each test in a database transaction and gives you a standard Django test client. DRF’s APITestCase subclasses it and swaps the client for APIClient, which understands DRF authentication schemes, content negotiation, and the format argument on requests. Use APITestCase for testing DRF endpoints.

When should I use force_authenticate instead of login?

Use force_authenticate() when you want to test view and permission logic without the cost and complexity of the real credential flow. It attaches a user to the request directly. Use login() or credentials() when the authentication mechanism itself, such as session or token auth, is what you want to verify.

Do DRF tests need a running server?

No. APIClient dispatches requests in-process directly to your views, so the test suite runs without starting a server. That is what makes it fast. To test the actually deployed service, including infrastructure like proxies and CORS, you send real HTTP requests with an API client such as Apidog.

How do I check test coverage for a DRF project?

Install the coverage package, then run coverage run --source='.' manage.py test followed by coverage report for a summary or coverage html for a line-by-line view. The HTML report highlights untested lines so you can see exactly which views or serializers lack tests.

Should I test serializers and endpoints separately?

Yes. Serializer unit tests are fast and pinpoint validation bugs without HTTP overhead. Endpoint tests with APITestCase check routing, permissions, and the full request cycle. Keeping both gives you quick feedback at the unit level and confidence that the wiring works at the integration level.

Practice API Design-first in Apidog

Discover an easier way to build and use APIs

How to Test a Django REST Framework API