In Django REST Framework, how do you write an APITestCase for an authenticated create endpoint, and what should it assert?
answer
- self.client is already an APIClient
- skip login, keep the request
- encode the body as JSON
- status, body, then the database row
basics
~10 sSubclass APITestCase, call self.client.force_authenticate(user=user), POST the payload with format='json', then assert status 201, the key fields of response.data, and that the database row exists with the right owner.
solid answer
~30 sI subclass `APITestCase`, whose `self.client` is an `APIClient`, and call `self.client.force_authenticate(user=self.user)` so the request arrives authenticated without a real login. I post with `self.client.post(reverse('claim-list'), payload, format='json')` so nested data and nulls survive. Then I assert three things: `response.status_code == status.HTTP_201_CREATED` with `response.data` as the failure message, the fields the client needs in `response.data` (the new `id`, the server-set `owner`), and the row itself in the database with `owner == self.user`. I add a second test proving an anonymous POST is rejected and creates nothing, because DRF's default permission is `AllowAny`.
code
python · 33 linesfrom decimal import Decimal
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from claims.models import ExpenseClaim
class CreateExpenseClaimTests(APITestCase):
@classmethod
def setUpTestData(cls):
cls.user = get_user_model().objects.create_user(username="dana")
def test_authenticated_user_creates_claim(self):
self.client.force_authenticate(user=self.user)
payload = {"amount": "42.50", "description": "Train ticket"}
response = self.client.post(reverse("claim-list"), payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
self.assertEqual(response.data["amount"], "42.50")
claim = ExpenseClaim.objects.get(pk=response.data["id"])
self.assertEqual(claim.owner, self.user)
self.assertEqual(claim.amount, Decimal("42.50"))
def test_anonymous_post_is_rejected(self):
response = self.client.post(reverse("claim-list"), {"amount": "1.00"}, format="json")
# SessionAuthentication is first in DRF's defaults, so this is 403, not 401.
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertFalse(ExpenseClaim.objects.exists())go deeper
Recall the pieces: APITestCase gives you an APIClient, force_authenticate(user=...) logs in for the test, format='json' encodes the body, and you assert status plus data.
Explain why the database row is asserted separately from response.data, and why an anonymous request gets 403 rather than 401 under DRF's default authentication classes.
Show you separate fast business-logic tests using force_authenticate from a small set of real-authentication tests, and that you catch AllowAny left as the default permission.
Frame the team convention: which assertions every create endpoint test must carry, and where the forced-authentication shortcut is allowed versus where real credentials are required.
## What the question is really checking An interviewer who asks this wants to see that you can test a DRF endpoint **through its public surface** — URL, HTTP method, request body, status code, response body — and that you also check the **side effect** the endpoint exists for: a row in the database, owned by the right user. The running example here is an expense-claims API: `POST /api/claims/` creates an `ExpenseClaim` whose `owner` is taken from `request.user`, never from the payload. Three pieces of `rest_framework.test` do the work: - **`APITestCase`** — Django's `TestCase` with `client_class = APIClient`, so `self.client` inside every test method is a fresh **`APIClient`**. Each test runs inside a transaction that is rolled back afterwards (that behaviour belongs to Django's `TestCase`, which `APITestCase` inherits unchanged). - **`APIClient.force_authenticate(user=...)`** — marks every following request from that client as coming from `user`, without building a token, session or password login. - **`format="json"`** on `post()` — encodes the payload with DRF's `JSONRenderer` and sets `Content-Type: application/json`, instead of the default multipart form encoding. ## The shape of the test 1. **Arrange** — create the user once, ideally in `setUpTestData`, and call `self.client.force_authenticate(user=self.user)` in the test. 2. **Act** — `self.client.post(reverse("claim-list"), payload, format="json")`. `reverse()` with the router's name (`<basename>-list`) keeps the test from hard-coding the URL. 3. **Assert the status** — `self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)`. Passing `response.data` as the message means a failure prints DRF's validation errors instead of just "400 != 201". 4. **Assert the body** — `response.data` is the primitive data the view put into its `Response` (for a `ModelViewSet.create`, the serializer's `.data`). Check the fields the client relies on, such as the new `id` and the server-set `owner`. 5. **Assert the side effect** — load the row from the database and check `owner == self.user`. A response can say 201 while `perform_create()` forgot to pass `owner=self.request.user`, if the serializer happens to accept an owner from the payload. ## The negative test belongs in the same class A create endpoint needs at least one test proving that **an anonymous client cannot create anything**. This is where many suites discover that the view never declared `permission_classes`: DRF's default `DEFAULT_PERMISSION_CLASSES` is `['rest_framework.permissions.AllowAny']`, so without `IsAuthenticated` on the view (or in `REST_FRAMEWORK` settings) the anonymous POST reaches `perform_create()`. Which status the anonymous POST gets depends on the **first authentication class** of the view: | First authentication class | Sends `WWW-Authenticate`? | Anonymous request is rejected with | |---|---|---| | `SessionAuthentication` (first in DRF's defaults) | No | `403 Forbidden` | | `BasicAuthentication` | Yes (`Basic realm="api"`) | `401 Unauthorized` | | `TokenAuthentication` | Yes (`Token`) | `401 Unauthorized` | DRF raises `NotAuthenticated` in both cases and coerces it to 403 when there is no header to send, so assert the exact code your configuration produces rather than "401 or 403". ## Assertions worth adding, and ones to avoid - **Do** assert that the count of `ExpenseClaim` rows is unchanged after a rejected request. - **Do** assert server-computed fields (`owner`, `status`, timestamps set by the model) rather than echoing back the payload you sent. - **Do** remember that `response.data` holds serializer output: a `DecimalField` comes back as the string `"42.50"` because `COERCE_DECIMAL_TO_STRING` defaults to `True`. - **Avoid** comparing the entire `response.data` dict with a literal when it contains ids or timestamps; it makes the test fail on every unrelated field added later. - **Avoid** asserting only the status code — a 201 with the wrong owner is the bug the test exists to catch. ## Where the setup lines belong - **The user** — create it in `setUpTestData`, which runs once per test class rather than once per test method; Django's `TestCase` gives each test its own copy of class-level attributes such as `cls.user`, so one test changing the user does not leak into the next. - **The forced login** — call `force_authenticate()` in the test method or in `setUp`, not in `setUpTestData`: the test case builds a new `APIClient` before every test, so a forced user does not carry over from one test to the next. - **The URL** — resolve it with `reverse()` inside the test; a router renamed from `claims` to `expense-claims` then fails loudly at `reverse()` instead of silently hitting a 404. ## Where the shortcut stops `force_authenticate()` is the right default for business-logic tests because it keeps them independent of how clients log in. It does, however, **replace** the view's authentication classes for that request, so the token parsing, the session lookup and `SessionAuthentication`'s CSRF check never run. Keep a few separate tests that authenticate for real with `credentials()` or `login()`; the fast business-logic tests and the slower authentication tests answer different questions.
- Why check the database row when the response already says 201?Because the response only proves the view returned success. The claim's `owner` should come from `request.user` in `perform_create()`; if the serializer exposes `owner` as writable and the view forgets to set it, the API can return 201 with a wrong or client-chosen owner. Loading the row and comparing `claim.owner` with the forced user catches exactly that bug.
- The anonymous test expects 401 but gets 403. Is the endpoint broken?Not necessarily. DRF raises `NotAuthenticated` and then looks at the view's first authentication class: if it supplies a `WWW-Authenticate` header (Basic, Token) the response is 401, otherwise it is coerced to 403. With `SessionAuthentication` first, as in DRF's defaults, 403 is correct. Assert the code your configuration actually produces.
- How do you undo force_authenticate halfway through a test?Call `self.client.force_authenticate(user=None)`. On `APIClient`, passing neither a user nor a token also calls `logout()`, which clears stored `credentials()` and any session, so the next request is anonymous.
saying these in an interview costs you the question
- Asserting only the status code and never the created row or its owner
- Assuming DRF rejects anonymous writes by default without IsAuthenticated
- Expecting 401 for anonymous requests regardless of the authentication classes
- Using Django's plain TestCase and then calling self.client.force_authenticate
- Posting nested data without format='json' and blaming the serializer