loom.testing¶
- class loom.testing.GoldenHarness[source]¶
Bases:
objectExecutes use cases in isolation with fake repositories.
Allows injecting fake repo instances, forcing errors on specific methods, and asserting performance baselines — all without a real database.
Example:
harness = GoldenHarness() harness.inject_repo(IProductRepo, FakeProductRepo(), model=Product) harness.force_error(IProductRepo, "create", Conflict("duplicate")) result = await harness.run(CreateProductUseCase, payload={"name": "X"})
- inject_repo(interface, fake_instance, *, model=None)[source]¶
Register a fake repository instance for an interface type.
- Parameters:
- Return type:
None
- force_error(interface, method, error)[source]¶
Force a specific repository method to raise an error.
- simulate_system_error(interface, method)[source]¶
Simulate a
SystemErroron a specific repository method.
- async run(use_case_type, *, params=None, payload=None)[source]¶
Execute a use case with injected fake repositories.
- async run_with_baseline(use_case_type, *, params=None, payload=None, name, max_ms, baseline_dir)[source]¶
Execute a use case and assert it completes within a time baseline.
Writes a
<name>.jsonfile tobaseline_dirrecording the measured duration. RaisesAssertionErrorif execution exceedsmax_ms.- Parameters:
use_case_type (type[UseCase[Any, Any]]) – UseCase subclass to execute.
params (dict[str, Any] | None) – Primitive parameter values keyed by name.
payload (dict[str, Any] | None) – Raw dict for
Input()command construction.name (str) – Baseline identifier used as the filename.
max_ms (float) – Maximum allowed execution duration in milliseconds.
baseline_dir (Path) – Directory where baseline JSON files are written.
- Returns:
Result produced by the use case.
- Raises:
AssertionError – If elapsed time exceeds
max_ms.- Return type:
- class loom.testing.HttpTestHarness[source]¶
Bases:
objectBuilds a FastAPI
TestClientwith injected fake repositories.Combines
bootstrap_app()andcreate_fastapi_app()into a single test-oriented entry point.Repositories are keyed by domain model type (the
TModelgeneric parameter of the use case) rather than by interface. The harness derives the correct binding automatically by scanning the use cases declared in the REST interfaces.- Parameters:
None
Example:
harness = HttpTestHarness() harness.inject_repo(Product, InMemoryRepository(Product)) harness.force_error(Product, "get_by_id", NotFound("not found")) client = harness.build_app(interfaces=[ProductRestInterface]) resp = client.get("/products/99") assert resp.status_code == 404
- inject_repo(model, fake)[source]¶
Register a fake repository for a domain model type.
The harness resolves the model from the use-case generic parameter (
UseCase[Model, …]) at bootstrap time and injectsfakeas its repository.- Parameters:
- Return type:
None
Example:
harness.inject_repo(Product, InMemoryRepository(Product)) harness.inject_repo(Order, FakeOrderRepo())
- force_error(model, method, error)[source]¶
Force a repository method to raise a specific error on every call.
Errors that are
LoomErrorsubclasses are mapped to the corresponding HTTP status code by the router.- Parameters:
- Return type:
None
Example:
from loom.core.errors import NotFound harness.force_error(Product, "get_by_id", NotFound("not found"))
- simulate_system_error(model, method)[source]¶
Simulate an unhandled
SystemErroron a repository method.The resulting HTTP response will have status
500.
- build_app(interfaces, **fastapi_kwargs)[source]¶
Build a
TestClientfrom REST interface declarations.Extracts use cases from the interface route declarations, bootstraps the application with the injected fake repositories, and returns a
TestClientready for HTTP assertions.The client is configured with
raise_server_exceptions=Falseso that unhandled server errors are returned as500responses instead of being re-raised in the test.- Parameters:
interfaces (Sequence[type[RestInterface[Any]]]) –
RestInterfacesubclasses whose routes should be exposed. Use cases are derived automatically from theirroutesdeclarations.**fastapi_kwargs (Any) – Forwarded to the
FastAPIconstructor (e.g.title,version).
- Returns:
TestClientwrapping the test app.- Return type:
fastapi.testclient.TestClient
Example:
client = harness.build_app(interfaces=[ProductRestInterface]) resp = client.post("/products/", json={"name": "Widget"}) assert resp.status_code == 201
- class loom.testing.InMemoryRepository(entity_type, *, id_field='id', creator=None)[source]¶
Bases:
Generic[T]Generic in-memory repository for testing any
msgspec.Structmodel.Stores entities in a plain dict keyed by their id field value. Provides the standard repository surface (
get_by_id,create,update,delete,list_paginated) without any database dependency.The
createmethod derives entity fields from the command automatically when nocreatorcallable is provided: fields present on the command are copied to the entity, and the id field is assigned from an internal auto-increment counter.- Parameters:
entity_type (type[T]) – The
msgspec.Structsubclass this repository stores.id_field (str) – Name of the identity field on the entity. Defaults to
"id".creator (Callable[[Any, int], T] | None) – Optional
(cmd, next_id) -> Tcallable used bycreate(). When provided, the automatic field-mapping is bypassed entirely.
Example:
repo = InMemoryRepository(Product, id_field="id") repo.seed(Product(id=1, name="Widget"), Product(id=2, name="Gadget")) harness = HttpTestHarness() harness.inject_repo(Product, repo) client = harness.build_app(interfaces=[ProductRestInterface])
- seed(*entities)[source]¶
Pre-load entities into the store.
The internal id counter is advanced past the highest integer id seen so that subsequent
create()calls do not collide.- Parameters:
*entities (T) – Entity instances to load.
- Return type:
None
Example:
repo.seed(Product(id=1, name="A"), Product(id=2, name="B"))
- async get_by_id(obj_id, profile='default')[source]¶
Return the entity with
obj_id, orNoneif not found.
- async create(cmd)[source]¶
Create and store a new entity from
cmd.If a
creatorcallable was provided at construction it is called ascreator(cmd, next_id). Otherwise, command attributes whose names match entity fields are copied automatically, and the id field is set from the internal auto-increment counter.- Parameters:
cmd (Any) – Command or payload object carrying the new entity’s data.
- Returns:
The created and stored entity.
- Return type:
T
- async update(obj_id, data)[source]¶
Update the entity at
obj_idwith fields fromdata.Only non-
Nonefields present on bothdataand the entity are overwritten; the id field is never changed.
- class loom.testing.RepositoryIntegrationHarness(*, session_manager, entities, load_order=())[source]¶
Bases:
objectTest harness for integration tests over repository implementations.
- Parameters:
session_manager (SessionManager)
entities (Mapping[str, EntityHarness])
- class loom.testing.UseCaseTest(use_case)[source]¶
Bases:
Generic[ResultT]Fluent test harness for executing UseCases without HTTP or framework overhead.
Builds and runs the real ExecutionPlan — no shortcuts or mocking of the pipeline. Designed for unit and integration tests that must exercise computes, rules, and load steps in full.
- Parameters:
use_case (UseCase[Any, ResultT]) – Constructed UseCase instance to test.
Example:
result = await ( UseCaseTest(UpdateUserUseCase(repo=fake_repo)) .with_params(user_id=1) .with_input(email="new@example.com") .run() )
- with_params(**kwargs)[source]¶
Set primitive parameter values bound by name.
- Parameters:
**kwargs (Any) – Parameter names and values matching the UseCase’s non-Input, non-Load parameters.
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_input(**kwargs)[source]¶
Set raw payload fields for command construction.
The payload is passed to the Command’s
from_payloadmethod. Usewith_commandif you have a pre-built Command instance.- Parameters:
**kwargs (Any) – Payload fields matching the Command struct.
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_command(cmd)[source]¶
Set a pre-built Command instance as the execution payload.
Serializes the command via
msgspec.to_builtinsso it is compatible with the standardfrom_payloadpipeline.- Parameters:
cmd (Any) – A
Command(msgspec.Struct) instance.- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_loaded(entity_type, entity)[source]¶
Pre-load an entity, bypassing repository calls for this type.
- Parameters:
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_deps(entity_type, repo)[source]¶
Register a repository for a given entity type.
Used when the UseCase has
LoadById()steps that require a repo.with_loadedtakes precedence overwith_depsfor the same type.- Parameters:
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- with_main_repo(repo)[source]¶
Inject the main repository dependency into the UseCase instance.
This is useful for unit tests of
UseCase[TModel, TResult]where the core logic reads fromself.main_repo.- Parameters:
repo (RepoFor[Any]) – Repository instance compatible with the UseCase’s main model.
- Returns:
selffor chaining.- Return type:
UseCaseTest[ResultT]
- async run()[source]¶
Compile and execute the UseCase through the full pipeline.
- Returns:
The result produced by the UseCase.
- Raises:
loom.core.errors.RuleViolations – If one or more rule steps fail.
NotFound – If a Load step finds no entity.
CompilationError – If the UseCase fails structural validation.
- Return type:
ResultT
- property plan: ExecutionPlan¶
Compile and return the ExecutionPlan for the UseCase.
Useful for asserting plan structure in advanced test scenarios.
- Returns:
The compiled
ExecutionPlan.
- loom.testing.build_repository_harness(*, session_manager, models, repositories=None, load_order=())[source]¶
Build a repository integration harness with generic/default repositories.
- Parameters:
- Returns:
Configured repository integration harness.
- Return type:
- loom.testing.serialize_plan(plan)[source]¶
Produce a deterministic, JSON-serialisable snapshot of an
ExecutionPlan.All keys are sorted alphabetically so the output is stable across runs regardless of insertion order. Types and callables are encoded as their fully qualified
module.qualnamestring.- Parameters:
plan (ExecutionPlan) – Compiled execution plan to serialise.
- Returns:
Dictionary containing only JSON-primitive values (str, int, list, dict, None). Suitable for
json.dumpscomparison.- Return type:
Example:
snapshot = serialize_plan(compiler.get_plan(MyUseCase)) assert snapshot["use_case"] == "my_app.use_cases.MyUseCase"