Testing tools#
Django already provides a mature set of
testing tools
and
advanced testing utilities.
django-modern-rest builds on these primitives instead of replacing them.
It provides JSON-oriented request factories and test clients for both sync and
async code, together with a bundled pytest plugin. All DMR testing tools use
the same request API as their Django counterparts.
Choosing a tool#
Choose the narrowest tool that exercises the behavior you need:
Tool |
Test level |
Use it when |
|---|---|---|
Sync unit test |
You want to call a sync controller directly without URL routing or middleware. |
|
Async unit test |
You want the same focused test for an async controller. |
|
Sync integration test |
URL routing, middleware, and the complete Django request-response cycle are part of the behavior under test. |
|
Async integration test |
You need the complete request-response cycle with an async test client. |
Request factories are usually the fastest choice for focused controller tests. Use a test client when routing, middleware, or another integration boundary matters.
Request factories#
DMRRequestFactory extends
django.test.RequestFactory and creates WSGI requests.
DMRAsyncRequestFactory extends
django.test.AsyncRequestFactory and creates ASGI requests.
Like Django request factories, they create a request object but do not send it
through URL routing or middleware. Pass the request directly to
Controller.as_view() instead. For async controllers, create the request
synchronously and await the controller response. Use
DMRAsyncRequestFactory.wrap() when static type checkers need help
understanding that the controller returns an awaitable.
The following tabs test the same controller with Django’s request factory,
DMR’s request factory, and the DMR pytest fixture:
1import json
2from http import HTTPStatus
3
4from dirty_equals import IsUUID
5from django.http import HttpResponse
6from django.test import RequestFactory, SimpleTestCase
7
8from examples.testing.pydantic_controller import UserController
9
10
11class DjangoRequestFactoryTests(SimpleTestCase):
12 def test_request_factory(self) -> None:
13 request_data = {'email': 'test@example.com', 'age': 43}
14 request = RequestFactory().post(
15 '/users/',
16 data=request_data,
17 content_type='application/json',
18 )
19
20 response = UserController.as_view()(request)
21
22 assert isinstance(response, HttpResponse)
23 self.assertEqual(response.status_code, HTTPStatus.CREATED)
24 self.assertEqual(response.headers, {'Content-Type': 'application/json'})
25 self.assertEqual(
26 json.loads(response.content),
27 {
28 'uid': IsUUID,
29 **request_data,
30 },
31 )
1import json
2from http import HTTPStatus
3
4from dirty_equals import IsUUID
5from django.http import HttpResponse
6from django.test import SimpleTestCase
7
8from dmr.test import DMRRequestFactory
9from examples.testing.pydantic_controller import UserController
10
11
12class DMRRequestFactoryTests(SimpleTestCase):
13 def test_request_factory(self) -> None:
14 request_data = {'email': 'test@example.com', 'age': 43}
15 request = DMRRequestFactory().post('/users/', data=request_data)
16
17 response = UserController.as_view()(request)
18
19 assert isinstance(response, HttpResponse)
20 self.assertEqual(request.content_type, 'application/json')
21 self.assertEqual(response.status_code, HTTPStatus.CREATED)
22 self.assertEqual(response.headers, {'Content-Type': 'application/json'})
23 self.assertEqual(
24 json.loads(response.content),
25 {
26 'uid': IsUUID,
27 **request_data,
28 },
29 )
The dmr_rf fixture provides a fresh
DMRRequestFactory.
1import json
2from http import HTTPStatus
3
4from dirty_equals import IsUUID
5from django.http import HttpResponse
6
7from dmr.test import DMRRequestFactory
8from examples.testing.pydantic_controller import UserController
9
10
11def test_request_factory(dmr_rf: DMRRequestFactory) -> None:
12 request_data = {'email': 'test@example.com', 'age': 43}
13 request = dmr_rf.post('/users/', data=request_data)
14
15 response = UserController.as_view()(request)
16
17 assert isinstance(response, HttpResponse)
18 assert request.content_type == 'application/json'
19 assert response.status_code == HTTPStatus.CREATED
20 assert response.headers == {'Content-Type': 'application/json'}
21 assert json.loads(response.content) == {
22 'uid': IsUUID,
23 **request_data,
24 }
Test clients#
DMRClient extends django.test.Client, while
DMRAsyncClient extends django.test.AsyncClient.
Unlike request factories, test clients resolve the requested URL and execute
the Django middleware chain.
This makes them a better fit for integration tests. The following tabs exercise
the same endpoint with Django’s client, DMR’s client, and the DMR pytest
fixture:
1import uuid
2from http import HTTPStatus
3
4from dirty_equals import IsDatetime, IsInt
5from django.test import TestCase
6from django.urls import reverse
7
8
9class DjangoTestClientTests(TestCase):
10 def test_client(self) -> None:
11 # See `django_test_app/server/apps/model_simple/views/minimalistic.py`
12 request_data = {
13 'email': 'test@example.com',
14 'customer_service_uid': str(uuid.uuid4()),
15 }
16 response = self.client.post(
17 reverse('api:model_simple:user_minimalistic'),
18 data=request_data,
19 content_type='application/json',
20 )
21
22 self.assertEqual(response.status_code, HTTPStatus.CREATED)
23 self.assertEqual(response.headers['Content-Type'], 'application/json')
24 self.assertEqual(
25 response.json(),
26 {
27 'id': IsInt(),
28 'created_at': IsDatetime(iso_string=True),
29 **request_data,
30 },
31 )
1import uuid
2from http import HTTPStatus
3
4from dirty_equals import IsDatetime, IsInt
5from django.test import TestCase
6from django.urls import reverse
7
8from dmr.test import DMRClient
9
10
11class DMRTestClientTests(TestCase):
12 # Use DMRClient as Django's default test client.
13 client_class = DMRClient
14
15 def test_client(self) -> None:
16 # See `django_test_app/server/apps/model_simple/views/minimalistic.py`
17 request_data = {
18 'email': 'test@example.com',
19 'customer_service_uid': str(uuid.uuid4()),
20 }
21 response = self.client.post(
22 reverse('api:model_simple:user_minimalistic'),
23 data=request_data,
24 )
25
26 self.assertEqual(response.status_code, HTTPStatus.CREATED)
27 self.assertEqual(response.headers['Content-Type'], 'application/json')
28 self.assertEqual(
29 response.json(),
30 {
31 'id': IsInt(),
32 'created_at': IsDatetime(iso_string=True),
33 **request_data,
34 },
35 )
The dmr_client fixture provides a fresh DMRClient.
1import uuid
2from http import HTTPStatus
3
4import pytest
5from dirty_equals import IsDatetime, IsInt
6from django.urls import reverse
7
8from dmr.test import DMRClient
9
10
11@pytest.mark.django_db
12def test_client(dmr_client: DMRClient) -> None:
13 # See `django_test_app/server/apps/model_simple/views/minimalistic.py`
14 request_data = {
15 'email': 'test@example.com',
16 'customer_service_uid': str(uuid.uuid4()),
17 }
18 response = dmr_client.post(
19 reverse('api:model_simple:user_minimalistic'),
20 data=request_data,
21 )
22
23 assert response.status_code == HTTPStatus.CREATED, response.content
24 assert response.headers['Content-Type'] == 'application/json'
25 assert response.json() == {
26 'id': IsInt(),
27 'created_at': IsDatetime(iso_string=True),
28 **request_data,
29 }
Async tools#
For async tests, use DMRAsyncRequestFactory or
DMRAsyncClient. Their pytest equivalents are
dmr_async_rf and dmr_async_client. Calls made with the async client and
async controller responses must be awaited.
The DMR pytest plugin is registered automatically when pytest-django is
installed; no conftest.py configuration is required. See the
pytest plugin API reference for all available fixtures.
Sending data and checking responses#
JSON requests#
DMR request factories and clients use application/json as the default
Content-Type for requests with a body. Dictionaries, lists, and other
JSON-compatible values passed as data are serialized automatically. The
DMR examples above rely on this default.
When msgspec is installed, DMR uses it to serialize request data and parse
JSON responses. Otherwise, it falls back to Python’s json implementation.
Strings and bytes are treated as already encoded and are not serialized again.
Custom content types#
Pass content_type when the request body uses a different representation, as
in factory.post('/users/', data=body, content_type='application/xml'). You
can also set the header explicitly with
headers={'Content-Type': 'application/xml'}.
Checking responses#
Responses are regular Django django.http.HttpResponse instances.
Check their status, headers, and decoded body using the standard Django API, as
the test client example does.
response.json() requires the response Content-Type to be
application/json. Inspect response.content directly for other formats.
Once you have the decoded value, choose an assertion that captures its complete
stable contract without hard-coding runtime-generated data. The
test assertions guide explains how to use
inline-snapshot and dirty-equals for both cases.