Testing Strategies
Lexigram 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 lexigram-testing package provides the building blocks.
1. Fakes Over Mocks
Section titled “1. Fakes Over Mocks”Lexigram 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 type | Dependencies | Speed |
|---|---|---|
| Unit | Fakes for everything | Fastest |
| Integration | Real backends (Postgres, Redis) | Slower |
| End-to-end | A booted app via a test client | Slowest |
2. Built-in Fakes
Section titled “2. Built-in Fakes”lexigram-testing ships fakes for the common contracts:
from lexigram.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 pytestfrom lexigram.testing import FakeCachefrom my_app.services import OrderService
@pytest.mark.asyncioasync 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 NoneFakeEventBus records the events your code publishes; consult the lexigram-testing package docs for its inspection helpers.
3. Controlling Time
Section titled “3. Controlling Time”For expiry, scheduling, or timestamp logic, inject a deterministic clock instead of the system clock:
from datetime import datetime, UTCfrom lexigram.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.
4. Overriding Services in the Container
Section titled “4. Overriding Services in the Container”When you boot the real application but want to replace one dependency, use a testing_mode container and override():
from lexigram 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 lexigram import Applicationfrom lexigram.tenancy import TenancyModule
async with Application.boot(modules=[TenancyModule.stub()]) as app: ...5. Test Clients
Section titled “5. Test Clients”lexigram-testing provides per-subsystem test beds and clients that boot the relevant providers in-process:
| Client | Bed | For |
|---|---|---|
WebTestClient | WebTestBed | Sending requests to your controllers without a real server |
DatabaseTestClient | DatabaseTestBed | Repository tests against a throwaway database |
TaskTestClient | TaskTestBed | Enqueue/execute background tasks |
AITestClient | AITestBed | Drive AI services with a stubbed LLM |
from lexigram.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 == 200See the package docs for each bed’s exact setup options.
6. Protocol Compliance Suites
Section titled “6. Protocol Compliance Suites”A standout feature of lexigram-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 lexigram.testing import CacheBackendCompliancefrom 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.
7. Separating Test Tiers
Section titled “7. Separating Test Tiers”Mark slower, infrastructure-dependent tests so you can run the fast suite during development:
uv run pytest -m "not integration"Next Steps
Section titled “Next Steps”lexigram-testingpackage — full fixture and helper reference- Dependency Injection —
testing_modeandoverride() - Result Pattern — asserting on
Ok/Err