Django REST Framework Testing: APIClient and Test Cases

Django REST Framework Testing: APIClient and Test Cases

Django REST Framework provides its own testing tools on top of Django's standard test client. Understanding APIClient, APITestCase, and authentication helpers is essential for writing reliable API tests. This guide covers all the patterns you'll need.

APITestCase vs TestCase

APITestCase is DRF's equivalent of Django's TestCase. It swaps out the default test client for APIClient, which understands content negotiation and serializes request bodies as JSON automatically.

from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth import get_user_model

User = get_user_model()

class ProductAPITests(APITestCase):

    def test_list_products(self):
        response = self.client.get("/api/products/")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertIsInstance(response.data, list)

Using status.HTTP_200_OK instead of integer literals makes tests self-documenting and avoids magic numbers. DRF's status module covers all standard HTTP status codes.

Authentication Patterns

Getting authentication right is the most common source of flaky DRF tests. DRF gives you three approaches.

force_authenticate

The fastest option. Bypasses all authentication backends and directly sets the user on the request. Use this for the majority of authenticated endpoint tests.

class OrderAPITests(APITestCase):

    def setUp(self):
        self.user = User.objects.create_user("alice", password="secret")
        self.client.force_authenticate(user=self.user)

    def test_create_order(self):
        response = self.client.post("/api/orders/", {
            "product_id": 1,
            "quantity": 2,
        }, format="json")
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    def test_list_own_orders_only(self):
        other_user = User.objects.create_user("bob", password="x")
        Order.objects.create(user=self.user, total=10)
        Order.objects.create(user=other_user, total=20)

        response = self.client.get("/api/orders/")
        self.assertEqual(len(response.data), 1)
        self.assertEqual(response.data[0]["total"], "10.00")

Call self.client.force_authenticate(user=None) to reset to unauthenticated state within a test.

credentials

Use credentials when you need to test token or session authentication behavior specifically. This sets headers on every subsequent request.

from rest_framework.authtoken.models import Token

class TokenAuthTests(APITestCase):

    def setUp(self):
        self.user = User.objects.create_user("alice", password="secret")
        self.token = Token.objects.create(user=self.user)

    def test_authenticated_with_token(self):
        self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
        response = self.client.get("/api/profile/")
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_invalid_token_rejected(self):
        self.client.credentials(HTTP_AUTHORIZATION="Token invalidtoken123")
        response = self.client.get("/api/profile/")
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_no_token_rejected(self):
        response = self.client.get("/api/profile/")
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

Login (session auth)

For APIs that use session authentication:

def test_session_auth(self):
    self.client.login(username="alice", password="secret")
    response = self.client.get("/api/profile/")
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    self.client.logout()

Testing Serializers Directly

Serializers are pure Python — test them in isolation without going through the HTTP layer.

from django.test import TestCase
from myapp.serializers import ProductSerializer

class ProductSerializerTests(TestCase):

    def test_valid_data_passes(self):
        data = {
            "name": "Widget",
            "price": "9.99",
            "stock": 100,
        }
        serializer = ProductSerializer(data=data)
        self.assertTrue(serializer.is_valid(), serializer.errors)

    def test_negative_price_rejected(self):
        data = {"name": "Widget", "price": "-5.00", "stock": 10}
        serializer = ProductSerializer(data=data)
        self.assertFalse(serializer.is_valid())
        self.assertIn("price", serializer.errors)

    def test_serializer_output_fields(self):
        product = Product.objects.create(name="Gadget", price="19.99", stock=5)
        serializer = ProductSerializer(product)
        self.assertEqual(set(serializer.data.keys()), {"id", "name", "price", "stock"})
        # Verify no sensitive fields leak out
        self.assertNotIn("internal_cost", serializer.data)

    def test_nested_serializer(self):
        category = Category.objects.create(name="Electronics")
        product = Product.objects.create(name="Laptop", price="999", category=category)
        serializer = ProductSerializer(product)
        self.assertEqual(serializer.data["category"]["name"], "Electronics")

Testing serializers directly is much faster than going through views. Catch validation logic here, not in integration tests.

Testing Permissions

Permission classes need dedicated tests. Test both the allowed and denied cases for each permission.

from rest_framework.test import APITestCase
from rest_framework import status

class ProductPermissionTests(APITestCase):

    @classmethod
    def setUpTestData(cls):
        cls.regular_user = User.objects.create_user("alice", password="x")
        cls.admin_user = User.objects.create_user("admin", password="x", is_staff=True)
        cls.product = Product.objects.create(name="Widget", price=9, stock=10)

    def test_anyone_can_list_products(self):
        # Unauthenticated
        response = self.client.get("/api/products/")
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_authenticated_user_can_create_product(self):
        self.client.force_authenticate(user=self.regular_user)
        response = self.client.post("/api/products/", {
            "name": "New Widget",
            "price": "5.00",
            "stock": 50,
        }, format="json")
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)

    def test_unauthenticated_cannot_create_product(self):
        response = self.client.post("/api/products/", {
            "name": "New Widget",
            "price": "5.00",
            "stock": 50,
        }, format="json")
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_only_admin_can_delete_product(self):
        self.client.force_authenticate(user=self.regular_user)
        response = self.client.delete(f"/api/products/{self.product.pk}/")
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

    def test_admin_can_delete_product(self):
        self.client.force_authenticate(user=self.admin_user)
        product = Product.objects.create(name="Temp", price=1, stock=1)
        response = self.client.delete(f"/api/products/{product.pk}/")
        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

Testing Pagination

Don't assume the full list is returned. Test that pagination metadata is present and that navigation works.

class PaginationTests(APITestCase):

    @classmethod
    def setUpTestData(cls):
        Product.objects.bulk_create([
            Product(name=f"Product {i}", price=i, stock=10)
            for i in range(25)
        ])

    def test_default_page_size(self):
        response = self.client.get("/api/products/")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        # Assuming PAGE_SIZE = 10
        self.assertEqual(len(response.data["results"]), 10)
        self.assertIsNotNone(response.data["next"])
        self.assertIsNone(response.data["previous"])

    def test_second_page(self):
        response = self.client.get("/api/products/?page=2")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data["results"]), 10)
        self.assertIsNotNone(response.data["previous"])
class ProductFilterTests(APITestCase):

    @classmethod
    def setUpTestData(cls):
        cls.cat_a = Category.objects.create(name="Electronics")
        cls.cat_b = Category.objects.create(name="Clothing")
        cls.p1 = Product.objects.create(name="Laptop", price=999, category=cls.cat_a)
        cls.p2 = Product.objects.create(name="T-Shirt", price=25, category=cls.cat_b)

    def test_filter_by_category(self):
        response = self.client.get(f"/api/products/?category={self.cat_a.pk}")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        names = [p["name"] for p in response.data["results"]]
        self.assertIn("Laptop", names)
        self.assertNotIn("T-Shirt", names)

    def test_search_by_name(self):
        response = self.client.get("/api/products/?search=Laptop")
        results = response.data["results"]
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0]["name"], "Laptop")

Testing File Uploads

from django.core.files.uploadedfile import SimpleUploadedFile

class ProductImageTests(APITestCase):

    def setUp(self):
        self.user = User.objects.create_user("alice", password="x")
        self.client.force_authenticate(user=self.user)
        self.product = Product.objects.create(name="Widget", price=9, stock=10)

    def test_upload_product_image(self):
        image = SimpleUploadedFile(
            "test.jpg",
            b"fake-image-content",
            content_type="image/jpeg",
        )
        response = self.client.patch(
            f"/api/products/{self.product.pk}/",
            {"image": image},
            format="multipart",
        )
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.product.refresh_from_db()
        self.assertTrue(self.product.image.name.endswith(".jpg"))

Note the format="multipart" — file uploads require multipart encoding, not JSON.

Testing Error Responses

Verify that your API returns consistent error shapes.

class ErrorResponseTests(APITestCase):

    def test_404_response_shape(self):
        response = self.client.get("/api/products/99999/")
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
        self.assertIn("detail", response.data)

    def test_validation_error_shape(self):
        self.client.force_authenticate(user=User.objects.create_user("u", password="x"))
        response = self.client.post("/api/products/", {}, format="json")
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        # Expect field-level errors
        self.assertIn("name", response.data)
        self.assertIn("price", response.data)

Using APIRequestFactory

When you want to test a view in complete isolation — without URL routing — use APIRequestFactory.

from rest_framework.test import APIRequestFactory
from myapp.views import ProductViewSet

class ViewSetDirectTests(TestCase):

    def setUp(self):
        self.factory = APIRequestFactory()
        self.user = User.objects.create_user("alice", password="x")

    def test_list_view_directly(self):
        request = self.factory.get("/api/products/")
        request.user = self.user
        view = ProductViewSet.as_view({"get": "list"})
        response = view(request)
        response.renderer_context = {}
        response.accepted_renderer = None
        self.assertEqual(response.status_code, 200)

APIRequestFactory is useful when testing view behavior in isolation, but APITestCase with self.client is preferable for most scenarios because it exercises the full middleware and URL routing stack.

Organizing DRF Tests

myapp/
    tests/
        test_serializers.py      # Serializer validation, output shape
        test_views.py            # Endpoint integration tests
        test_permissions.py      # Permission class coverage
        test_filters.py          # Filtering, search, ordering
        test_pagination.py       # Page size, navigation

Run DRF-specific tests:

python manage.py test myapp.tests.test_views
python manage.py test myapp.tests.test_serializers

Well-structured DRF tests catch contract breaks before they reach clients. For continuous verification against your deployed API — including authentication flows and permission boundaries — HelpMeTest can run end-to-end API checks on every deploy.

Read more

Start now free