Testing Strategies
Lexigram is designed with testability at its core. By using the lexigram-testing package, you can write tests that are completely decoupled from your infrastructure (DB, Cache, Queues) and run entirely in-process.
1. The Strategy: Fakes over Mocks
Section titled “1. The Strategy: Fakes over Mocks”In Lexigram, we prefer Fakes (real, in-memory implementations of protocols) over Mocks (magic objects that record calls). Fakes provide a more realistic environment while maintaining the speed of unit tests.
| Tool | Purpose | When to Use |
|---|---|---|
| Unit Tests | Logic testing | Use Fakes for all dependencies. |
| Integration Tests | Infrastructure verification | Use real containers (Redis, PG). |
| E2E Tests | System-wide verification | Use the TestClient to hit your API. |
2. Using Built-in Fixtures
Section titled “2. Using Built-in Fixtures”Lexigram’s pytest plugin automatically provides fixtures for all core services.
import pytestfrom lexigram.testing.clients.cache import FakeCacheBackendfrom lexigram.testing.clients.events import FakeEventBus
@pytest.mark.asyncioasync def test_orders_are_cached(fake_cache: FakeCacheBackend, service: OrderService): # Perform action await service.place_order(id="123")
# Assert using the Fake's state cached = await fake_cache.get("order:123") assert cached is not None3. High-Fidelity Assertions
Section titled “3. High-Fidelity Assertions”Every Fake implementation in lexigram-testing includes helper methods to make assertions about what happened during the test.
Event Bus Assertions
Section titled “Event Bus Assertions”async def test_emit_event(fake_event_bus: FakeEventBus, service: OrderService): await service.place_order(id="123")
# helper for clean assertions assert fake_event_bus.published_count("OrderPlaced") == 1 event = fake_event_bus.last_published("OrderPlaced") assert event.order_id == "123"Task Queue Assertions
Section titled “Task Queue Assertions”async def test_enqueue_task(fake_queue: FakeTaskQueue, service: UserService): await service.register(email="admin@lexigram.dev")
assert fake_queue.enqueued_count("send_welcome_email") == 1 job = fake_queue.last_enqueued("send_welcome_email") assert job.kwargs["email"] == "admin@lexigram.dev"4. Testing the Web Layer
Section titled “4. Testing the Web Layer”Use the LexigramTestClient to test your controllers without starting a real HTTP server.
from lexigram.testing import LexigramTestClient
async def test_get_profile(client: LexigramTestClient): response = await client.get("/api/profile", headers={"X-Tenant-ID": "lexigram"})
assert response.status_code == 200 assert response.json()["user"] == "geezmo"5. Mocking Time
Section titled “5. Mocking Time”For tasks that depend on the current time (e.g., expiry logic), Lexigram provides a TimeService that can be frozen or advanced in tests.
from lexigram.testing.time import freeze_time
@freeze_time("2024-01-01 12:00:00")async def test_token_expiry(service: TokenService): token = await service.create_token() assert token.expires_at == "2024-01-01 13:00:00" # +1 hour[!TIP] Use the
@pytest.mark.integrationmarker to separate fast unit tests from slower infrastructure-dependent tests. You can then run them selectively:uv run pytest -m "not integration"
Available Fakes
Section titled “Available Fakes”FakeDatabase: In-memory SQL/NoSQL provider.FakeCacheBackend: In-memory dictionary-based cache.FakeEventBus: In-process pub/sub tracker.FakeTaskQueue: Background job tracker.FakeAIClient: Stubbed LLM that returns pre-configured responses.