Testing¶
Introduction¶
Waku provides utilities to simplify testing DI-heavy applications. Instead of manually wiring up containers
and modules for every test, two helpers in waku.testing let you spin up isolated test applications
and swap providers on the fly:
create_test_app()— an async context manager that builds a fully initializedWakuApplicationfrom minimal configuration.override()— a sync context manager that temporarily replaces providers (or context values) inside a live container.
Together they cover the two most common testing scenarios: creating a throwaway app with fakes, and patching a long-lived app fixture for a single test.
create_test_app()¶
Signature¶
@asynccontextmanager
async def create_test_app(
*,
base: ModuleType | DynamicModule | None = None,
providers: Sequence[Provider] = (),
imports: Sequence[ModuleType | DynamicModule] = (),
extensions: Sequence[ModuleExtension] = (),
app_extensions: Sequence[ApplicationExtension] = DEFAULT_EXTENSIONS,
context: dict[Any, Any] | None = None,
) -> AsyncIterator[WakuApplication]: ...
create_test_app() is an async context manager that yields a fully initialized WakuApplication.
On exit, all lifecycle hooks (shutdown, destroy) run automatically.
| Parameter | Description |
|---|---|
base |
An existing module to build upon. When provided, providers are marked as overrides so they replace matching registrations from the base module. |
providers |
Providers to register in the internal test module. |
imports |
Additional modules to import alongside base. |
extensions |
Module extensions to attach to the test module. |
app_extensions |
Application-level extensions (defaults to DEFAULT_EXTENSIONS). |
context |
Context values forwarded to the DI container. |
Basic usage¶
Create a standalone test app with the exact providers you need:
Overriding a production module¶
Pass an existing module as base and supply replacement providers.
The replacements are automatically marked as overrides, so Dishka resolves them
instead of the originals:
Tip
When base is not provided, providers are registered normally (not as overrides).
Use base only when you want to reuse an existing module and selectively replace some of its providers.
override()¶
Signature¶
@contextmanager
def override(
container: AsyncContainer,
*providers: BaseProvider,
context: dict[Any, Any] | None = None,
) -> Iterator[None]: ...
override() is a sync context manager that temporarily swaps providers and/or context values
in a live AsyncContainer. When the with block exits, the original container state is restored.
| Parameter | Description |
|---|---|
container |
The container to override. Must be at APP scope (application.container). |
*providers |
Replacement providers. |
context |
Context values to override or add. Existing context values not listed here are preserved. |
Warning
override() only works on the root (APP scope) container. Passing a request-scoped container
raises ValueError. Always use application.container, not a container obtained from
async with application.container().
Replacing a provider¶
Overriding context values¶
You can also override context values without touching providers. When only context is overridden (no providers), the existing provider cache is preserved for better performance:
| test_override_context.py | |
|---|---|
Fixture patterns¶
Session-scoped application fixture¶
Create the application once per test session and reuse it across tests.
Use override() in individual tests to swap out specific providers:
| conftest.py | |
|---|---|
Per-test overrides¶
Combine the session-scoped fixture with override() to keep tests isolated without
rebuilding the entire application each time:
Further reading¶
- Event Sourcing Testing —
DeciderSpecGiven/When/Then DSL for testing deciders and aggregates - Dishka testing documentation — alternative testing approaches provided by the underlying DI framework