Testing#
next.dj ships next.testing with a test client, registry isolation, signal capture, action helpers, and HTML utilities.
This page covers the public surface of the module and the patterns for testing pages, components, forms, and signals end to end.
Choose the right helper#
next.testing groups its helpers into focused submodules.
The submodules cover the client, isolation, signal capture, rendering, loaders, HTML assertions, patching, action helpers, and dependency context builders.
The table below maps each testing goal to the helper and its import path.
Goal |
Use |
Import |
|---|---|---|
HTTP request to a page or action |
|
|
POST to a registered action by name |
|
|
GET a URL as a partial zone request |
|
|
Decode a partial response for structural assertions |
|
|
Inspect ops, targets, and assets of a patch envelope |
|
|
Render a page body without HTTP |
|
|
Render a component in isolation |
|
|
Assert on rendered HTML structure |
|
|
Capture one or more signals explicitly |
|
|
Capture every framework signal at once |
|
|
Inspect a captured signal payload |
|
|
Validate a form without HTTP |
|
|
Temporarily override |
|
|
Wrap the static collector for assertions |
|
|
Unit-test a custom provider or resolver path |
|
|
Force-import pages or components in tests |
|
|
Reload backends after mutating settings or registries |
|
|
Clear the form registries, diagnostics, and wizard backend |
|
|
You can import everything above from the next.testing package.
Submodule imports stay valid when you prefer explicit paths.
See Testing reference for generated signatures.
Boot the suite#
The next.testing helpers assume the app registry is populated before any helper is imported.
- Pytest.
Set
DJANGO_SETTINGS_MODULEinpytest.inisopytest-djangocan configure Django before collecting tests. Run the suite withuv run pytest.pytest.ini#[pytest] DJANGO_SETTINGS_MODULE = config.settings python_files = test_*.py
- Stdlib
unittest. Call
django.setup()once before importing anynext.testinghelper, then run the suite with the standard runner.
Registry state between tests#
Action and component registrations are side effects of importing page.py and component.py modules.
The canonical scaffold imports them once per session with the eager loaders from next.testing.loaders and leaves the registries alone between tests.
from pathlib import Path
import pytest
from next.testing import eager_load_pages
PROJECT_ROOT = Path(__file__).resolve().parent
@pytest.fixture(autouse=True, scope="session")
def _load_pages() -> None:
eager_load_pages(PROJECT_ROOT / "notes" / "pages")
The session fixture runs the @context and @action decorators before the first test dispatches a request.
Every project under examples/ uses this scaffold.
Note
When LAZY_COMPONENT_MODULES = True in NEXT_FRAMEWORK, bulk import of component.py modules from configured component roots is skipped during AppConfig.ready.
Call eager_load_components() from next.testing.loaders once per session to import every registered component.py regardless of the flag.
import pytest
from next.testing.loaders import eager_load_components
@pytest.fixture(autouse=True, scope="session")
def _load_components() -> None:
eager_load_components()
With the default LAZY_COMPONENT_MODULES = False, all registrations are in place after AppConfig.ready, so the extra call is unnecessary.
See Settings for the full description of LAZY_COMPONENT_MODULES.
Resetting registries#
reset_registries() is an opt-in helper for tests that mutate NEXT_FRAMEWORK or the registries themselves.
It reloads the form-action and component backends from the current settings.
Two narrower helpers reset a single registry.
reset_components()reloads only the component backends, throughComponentsManager.reload.reset_form_actions()reloads only the form-action backends, throughFormActionManager.reload.
Warning
A reset does not bring import-time registrations back.
Python does not re-import a module that is already in sys.modules, so @action handlers and page-tree components registered by earlier imports stay absent after the reset.
Do not call reset_registries() from an autouse fixture in an ordinary suite.
Reserve it for tests that verify registry behaviour itself.
A third helper, reset_page_cache(), resets no registry.
It calls Page.clear_template_caches to drop the page template cache and is useful when a test rewrites template files on disk.
For tests that probe registration itself, reset_form_registration_state() clears every form registry, the registration diagnostics buffer, and resets the wizard backend in one call.
Tests that write template.djx or page.py files to tmp_path register fresh state on every run, so a suite dedicated to them pairs a registry reset with a page-cache reset.
import pytest
from next.testing.isolation import reset_page_cache, reset_registries
@pytest.fixture(autouse=True)
def _isolation():
reset_registries()
yield
reset_registries()
reset_page_cache()
Eager page loading#
eager_load_pages(base_dir) imports every page.py under a given directory.
Use it when a test suite does not go through the full request cycle and must trigger @context and @action side-effects manually.
clear_loaded_dirs() drops the per-directory memoisation so a later eager_load_pages call re-imports.
It is intended for self-tests of the loader and for the rare case of rewritten page.py files within one session.
A normal test suite does not call it, since each pytest session starts a fresh interpreter.
NextClient#
NextClient is a thin subclass of Django’s Client that adds post_action, get_action_url, and get_zones.
post_action and get_action_url resolve an action name through resolve_action_url before delegating to the underlying client.
from next.testing.client import NextClient
def test_index() -> None:
response = NextClient().get("/")
assert response.status_code == 200
The client mirrors Django’s Client API.
get, post, put, delete, and patch all work.
Pass follow=True to a request to follow redirects, exactly as with Django’s Client.
Posting to actions#
NextClient.post_action resolves an action name to its URL and posts the data in one call.
from next.testing.client import NextClient
def test_create_note(db) -> None:
response = NextClient().post_action("create_note", {"title": "Test", "body": ""})
assert response.status_code == 302
The origin keyword fills the hidden _next_form_origin field the {% form %} tag emits in the browser, the page URL the dispatcher resolves to re-render on a validation failure.
def test_blank_title_rerenders(db) -> None:
response = NextClient().post_action("create_note", {"title": ""}, origin="/")
assert response.status_code == 200
A value already present in data under _next_form_origin wins over the keyword, so protocol-level tests can drive the raw field directly, including posting without it to assert the HTTP 400 rejection.
NextClient.get_action_url returns the dispatch URL without posting, for tests that need the URL itself.
Both methods resolve the name through resolve_action_url from next.testing.actions.
An unknown name raises FormActionNotFoundError from next.forms.
Partial requests#
The client drives partial rendering without hand-built headers.
Pass partial=True and zones= to post_action to turn the POST into a patch request scoped to a zone.
zones accepts one name or a tuple of names.
from next.testing.client import NextClient, envelope_of
def test_partial_morph(db) -> None:
response = NextClient().post_action(
"create_note",
{"title": "hi"},
partial=True,
zones="notes",
)
envelope = envelope_of(response)
assert "notes" in envelope.zone_targets()
NextClient.get_zones(url, zones) GETs a URL as a partial request for the named zones.
zones is one name or a tuple of names.
Both post_action and get_zones accept a version= keyword that stamps the X-Next-Version header so tests can drive the version-sync branch.
from next.testing.client import NextClient, envelope_of
def test_zone_get() -> None:
response = NextClient().get_zones("/notes/", "notes")
envelope = envelope_of(response)
assert envelope.zone_targets() == ["notes"]
envelope_of(response) decodes a patch response into a PartialEnvelope.
It raises when the response is not a patch envelope, so a navigation fallback never passes a structural assertion.
PartialEnvelope exposes version, ops, and assets, plus op_verbs, targets, zone_targets, form_targets, form_meta, toasts, and html_for_zone for asserting on the server contract without parsing HTML.
See Partial rendering for the zone and patch model these helpers exercise.
Render a page#
Use next.testing.rendering to render a page without an HTTP round trip.
from next.testing.rendering import render_page
def test_index_body() -> None:
html = render_page("notes/pages/page.py")
assert "Notes" in html
render_page reads the static body source, the template attribute or a registered file template such as template.djx, then runs context functions and the static collector.
It does not invoke a render() function declared in page.py.
Use NextClient for pages whose body is built by render().
Use it for snapshot tests and template assertion tests that do not need URL routing.
Pass an HttpRequest as the second positional argument to supply a custom request.
When omitted the helper synthesises one through RequestFactory().get("/") so context functions and the static collector see a real request object.
Extra keyword arguments are forwarded to the underlying page.render call as URL kwargs, which feeds them into DUrl markers and other URL-scoped providers.
from django.test import RequestFactory
from next.testing.rendering import render_page
def test_index_with_request() -> None:
request = RequestFactory().get("/?debug=1")
html = render_page("notes/pages/page.py", request)
assert "Notes" in html
Capture signals#
SignalRecorder subscribes to one or more signals on enter and unsubscribes on exit.
from next.signals import action_dispatched
from next.testing.client import NextClient
from next.testing.signals import SignalRecorder
def test_emits(db) -> None:
with SignalRecorder(action_dispatched) as recorder:
NextClient().post_action("create_note", {"title": "hi"})
assert len(recorder.events) == 1
event = recorder.events[0]
assert event.kwargs["action_name"] == "create_note"
The recorder holds a list of SignalEvent instances with signal, sender, and kwargs attributes.
SignalRecorder accepts one or more signals and exposes these public members.
events.The full list of captured
SignalEventinstances in emission order.start().Connects receivers for every tracked signal and returns the recorder. Called automatically on context entry.
stop().Disconnects receivers for every tracked signal. Called automatically on context exit.
events_for(signal).Returns the list of captured events emitted by that signal.
first_for(signal).Returns the first captured event for that signal, or raises
LookupErrorwhen none was captured.last_for(signal).Returns the last captured event for that signal, or raises
LookupErrorwhen none was captured.clear().Drops every captured event without disconnecting.
The recorder is also iterable and supports len() over the captured events.
Two convenience wrappers cover the common multi-signal cases.
capture_signals(*signals) returns a started SignalRecorder and reads well in with statements.
from next.signals import action_dispatched, page_rendered
from next.testing.client import NextClient
from next.testing.signals import capture_signals
def test_dispatch_and_render(db) -> None:
with capture_signals(action_dispatched, page_rendered) as recorder:
NextClient().post_action("create_note", {"title": "hi"})
assert len(recorder.events_for(action_dispatched)) == 1
dispatch = recorder.first_for(action_dispatched)
assert dispatch.kwargs["action_name"] == "create_note"
capture_framework_signals() attaches to every name in next.signals.__all__, which helps integration tests assert ordering without listing signals by hand.
Action helpers#
next.testing.actions exposes resolve_action_url and build_form_for.
resolve_action_url turns an action name into its dispatch URL.
build_form_for builds a bound form for an action so a unit test can assert validation without HTTP.
Both raise FormActionNotFoundError from next.forms for an unknown action name, with the closest registered names rendered into the message.
build_form_for raises LookupError for an action registered without a form class.
from next.testing.actions import build_form_for, resolve_action_url
def test_form_validates(db) -> None:
url = resolve_action_url("create_note")
form = build_form_for("create_note", {"title": "Direct", "body": ""})
assert form.is_valid()
HTML utilities#
next.testing.html provides assertions for inspecting rendered HTML fragments.
from next.testing.client import NextClient
from next.testing.html import assert_has_class, find_anchor
from next.testing.rendering import render_component_by_name
def test_index_links_to_note() -> None:
html = NextClient().get("/").content.decode()
anchor = find_anchor(html, text="First")
assert "First" in anchor
def test_card_class() -> None:
html = render_component_by_name(
"note_card",
at="notes/pages/page.py",
context={"note": {"title": "First"}},
)
assert_has_class(html, "note-card")
find_anchor returns the matching anchor tag and raises LookupError when no anchor matches the filters, see next.testing.html.find_anchor() for the accepted keywords.
assert_has_class and assert_missing_class check the class list of the first start tag in the fragment.
Patching#
next.testing.patching provides context managers that swap framework parts for the duration of a block.
Helper |
Effect |
|---|---|
|
Temporarily override |
|
Temporarily replace a named dependency value. |
|
Temporarily register a parameter provider. |
|
Temporarily register a form action. |
|
Temporarily swap the component backend configs. |
|
Temporarily swap the static collector implementation. |
|
Thin proxy around a collector for introspection in tests. |
A StaticCollectorProxy is yielded by patch_static_collector(capture=True).
Its .collector attribute holds the collector built inside the block, so a test can assert on the emitted styles and scripts without parsing HTML.
Pass factory= to swap the collector implementation entirely.
The callable runs in place of the default create_collector and returns a custom StaticCollector for the duration of the block.
Use patch_static_collector(capture=True) to inspect which assets a page emits.
from next.testing.patching import patch_static_collector
from next.testing.client import NextClient
def test_collects_styles() -> None:
with patch_static_collector(capture=True) as proxy:
NextClient().get("/")
assert proxy.collector is not None
styles = proxy.collector.assets_in_slot("styles")
assert len(styles) > 0
from next.testing.client import NextClient
from next.testing.patching import override_next_settings
def test_with_strict_context() -> None:
with override_next_settings(STRICT_CONTEXT=True):
response = NextClient().get("/")
assert response.status_code == 200
The patch reverts on exit, so the next test sees the original configuration.
override_dependency binds a stub value to a Depends("name") registration for the block, and restores the previous registration on exit.
from next.testing.client import NextClient
from next.testing.patching import override_dependency
def test_stubbed_theme() -> None:
with override_dependency("layout_theme", {"name": "Stub"}):
response = NextClient().get("/")
assert b"Stub" in response.content
override_provider prepends a provider instance to the resolver’s provider list for the block.
The prepended provider wins over every auto-registered provider that would otherwise claim the same parameter.
Implement the ParameterProvider protocol on a plain class for the stub, because subclassing RegisteredParameterProvider registers the provider globally.
from next.testing.deps import resolve_call
from next.testing.patching import override_provider
class EveryIntIsSeven:
def can_handle(self, param, context) -> bool:
return param.annotation is int
def resolve(self, param, context) -> int:
return 7
def count_notes(limit: int) -> int:
return limit
def test_provider_wins() -> None:
with override_provider(EveryIntIsSeven()):
kwargs = resolve_call(count_notes)
assert kwargs == {"limit": 7}
Resolution context doubles#
next.testing.deps.make_resolution_context builds a ResolutionContext for unit tests on providers.
next.testing.deps.resolve_call resolves a callable’s dependencies and returns the kwargs mapping.
Both accept the same loose keyword arguments, request, form, url_kwargs, and context_data.
from next.testing.deps import make_resolution_context
def test_context_carries_url_kwargs() -> None:
context = make_resolution_context(url_kwargs={"id": 7})
assert context.url_kwargs["id"] == 7
Pass resolve_call a callable whose annotated parameters a provider can fill, then assert on the returned mapping.
Use these helpers for testing custom providers without booting the router.
System checks#
Pytest can run manage.py check as part of the suite.
from django.core.management import call_command
def test_no_check_warnings() -> None:
call_command("check", verbosity=0)
See also#
See also
Test a page with actions for a full end to end flow, a form validation failure, and a signal emission assertion with working code. Testing reference for the public API. Signals for the signal catalog.