Skip to content

Testing Strategies

QuadKit is built for testability. Because services depend on protocols and are wired by the container, you can replace any infrastructure dependency with an in-memory implementation and run your tests entirely in-process. The quadkit-testing package provides the building blocks.

Terminal window
uv add --dev quadkit-testing

QuadKit favours fakes — real, in-memory implementations of a protocol — over mocks that merely record calls. Fakes behave like the real thing (you can read back what you wrote), so your tests exercise genuine logic at unit-test speed.

Test typeDependenciesSpeed
UnitFakes for everythingFastest
IntegrationReal backends (Postgres, Redis)Slower
End-to-endA booted app via a test clientSlowest

quadkit-testing ships fakes for the common contracts:

from quadkit.testing import (
FakeCache, # CacheBackendProtocol
FakeEventBus, # EventBusProtocol
FakeClock, # ClockProtocol (manually advanced)
FixedClock, # ClockProtocol (frozen at a fixed time)
FakeLogger,
FakeCommandBus,
FakeQueryBus,
FakeUnitOfWork,
FakeMetricsCollector,
)

Use a fake exactly where the real implementation would go — inject it as the protocol:

import pytest
from quadkit.testing import FakeCache
from my_app.services import OrderService
@pytest.mark.asyncio
async def test_order_is_cached():
cache = FakeCache()
service = OrderService(cache=cache)
await service.place_order(order_id="123")
# FakeCache implements CacheBackendProtocol — read the value straight back
cached = await cache.get("order:123")
assert cached.unwrap() is not None

FakeEventBus records the events your code publishes; consult the quadkit-testing package docs for its inspection helpers.


For expiry, scheduling, or timestamp logic, inject a deterministic clock instead of the system clock:

from datetime import datetime, UTC
from quadkit.testing import FixedClock
clock = FixedClock(datetime(2026, 1, 1, 12, 0, tzinfo=UTC))
service = TokenService(clock=clock)
token = await service.create_token(ttl_seconds=3600)
assert token.expires_at == datetime(2026, 1, 1, 13, 0, tzinfo=UTC)

FakeClock is the manually-advanceable variant when you need to simulate the passage of time within a test.


When you boot the real application but want to replace one dependency, use a testing_mode container and override():

from quadkit import Container
container = Container(testing_mode=True)
container.override(UserRepository, FakeUserRepository())

override() is only available when testing_mode=True, so it can never be used by accident in production.

Modules expose stub() for the same purpose at the module level — it returns a test-mode variant backed by in-memory/no-op providers:

from quadkit import Application
from quadkit.web.module import WebModule
async with Application.boot(modules=[WebModule.stub()]) as app:
...

quadkit-testing provides per-subsystem test beds and clients that boot the relevant providers in-process:

ClientBedFor
WebTestClientWebTestBedSending requests to your controllers without a real server
DatabaseTestClientDatabaseTestBedRepository tests against a throwaway database
TaskTestClientTaskTestBedEnqueue/execute background tasks
AITestClientAITestBedDrive AI services with a stubbed LLM
from quadkit.testing import WebTestBed
async def test_get_profile():
async with WebTestBed(create_app) as bed:
client = bed.client
response = await client.get("/api/profile")
assert response.status_code == 200

See the package docs for each bed’s exact setup options.


A standout feature of quadkit-testing: reusable compliance suites that verify your implementation of a protocol behaves correctly. Run the suite against your backend to guarantee it honours the contract:

from quadkit.testing import CacheBackendCompliance
from my_app.infrastructure.cache import MyCustomCache
class TestMyCacheCompliance(CacheBackendCompliance):
def make_backend(self):
return MyCustomCache()

Compliance suites ship for caches, repositories, event buses, queues, vector stores, search engines, audit stores, and more — so custom implementations stay drop-in compatible.


Mark slower, infrastructure-dependent tests so you can run the fast suite during development:

Terminal window
uv run pytest -m "not integration"