Test a Component
Run and extend the two test suites every scaffolded Component ships, so the pipeline’s behaviour is pinned by tests instead of by hope — including the behaviour a local run cannot prove.
Before you begin
Section titled “Before you begin”- A scaffolded Component to work in (the quick start’s
order-extractoris used here; every Component scaffold has the same layout) - .NET 10 SDK — and nothing else. The tests run without Docker, without a Dapr sidecar, and without the system host
- The Intropy Framework page explains the step and result types the assertions use
1. Run what the scaffold shipped
Section titled “1. Run what the scaffold shipped”Every component scaffold carries two test projects with a deliberate division of labour:
| Suite | Covers | Faked |
|---|---|---|
test/<Name>.Test.Unit |
One pipeline step at a time: pure logic, result variants | Nothing — steps take values in, return results |
test/<Name>.Test.Integration |
The composed pipeline, exactly as production builds it | Only the externals, via Intropy.Framework.Testing |
dotnet test ./test/OrderExtractor.Test.Unitdotnet test ./test/OrderExtractor.Test.Integrationtask test runs both; task coverage adds a Cobertura report. On a fresh empty=false scaffold everything is green. On an empty=true scaffold some integration tests fail by design — they are executable specifications for the step bodies you haven’t written yet, and they name the behaviour they are waiting for.
2. Unit-test a step
Section titled “2. Unit-test a step”A step is a class with one method, so a unit test needs no harness: construct it, call ExecuteAsync, assert on the result variant. The scaffold’s own tests show the shape:
var transformer = new Transformer();var order = new SourceOrder( OrderId: "ORD-1001", CustomerId: "CUST-42", OrderDate: orderDate, Lines: [new OrderLine("SKU-1", 2, 10.0m), new OrderLine("SKU-2", 1, 5.0m)]);
var (result, _) = await transformer.ExecuteAsync( order, new Context(new Dictionary<string, string>()), CancellationToken.None);
var success = Assert.IsType<TechnicalStepResult<Order>.Success>(result);Assert.Equal("ORD-1001", success.Value.OrderId);Assert the variant, not just the value: a validator’s failure case should be pinned as BusinessStepResult<T>.Failure carrying a BusinessIncidentData, because which variant a step returns decides what the runtime does next. Steps stay this easy to test as long as they stay pure — anything impure (the processing timestamp, an external call) belongs at the pipeline’s edges, which is exactly what the integration suite covers.
3. Integration-test the composed pipeline
Section titled “3. Integration-test the composed pipeline”The integration suite builds the production DI graph — the same Composition.ConfigureServices the system host runs — and swaps only the externals for fakes. The scaffold centralises that swap in one class so individual tests cannot drift:
public sealed class FakeEdges{ public InMemoryFileAdapter Files { get; } = new(); public FakeTopic<Context> Topic { get; } = new(); public FakeIdempotencyServiceClient Idempotency { get; } = new(); public FakeBusinessIncidentServiceClient Incidents { get; } = new();
public IServiceCollection CreateServices() { var services = Composition.Composition.ConfigureServices(new ServiceCollection()); services.RemoveAllKeyed<IFileAdapter>(Constants.SourceFileAdapterKey); services.AddKeyedSingleton<IFileAdapter>(Constants.SourceFileAdapterKey, (_, _) => Files); services.RemoveAll<IIdempotencyServiceClient>(); services.AddSingleton<IIdempotencyServiceClient>(Idempotency); // …incident client and publish edge swapped the same way return services; }}The fakes come from the Intropy.Framework.Testing package. Two details in this pattern are load-bearing:
- Remove, then add.
RemoveAllbeforeAddSingletonmeans the override never depends on registration order — a reorderedConfigureServicescannot silently shadow a fake with the production registration. - Fake only the externals. Everything between the edges (steps, builder, engine, short-circuiting) is the real production object graph. A test that fakes a step is testing the fake.
A test then drives the Component the way the runtime would, and asserts on fake state:
var (sweep, files, topic, idempotency, _) = CreateSut();files.AddFile("order-1.json", ValidOrderJson);
var summary = await sweep.SweepAsync(CancellationToken.None);
Assert.Equal(1, summary.Processed);var published = Assert.Single(topic.Events);Assert.Equal("ORD-1001", published.Subject);Assert.Empty(files.Files); // the source file was consumedAssert.Single(idempotency.Committed); // the run was recordedThe scaffold also ships a composition smoke test (CompositionTests) that just resolves the runner from the unmodified production graph — registrations are lazy, so a missing or renamed registration fails there instead of at system-host startup.
4. Pin the behaviour the local run cannot prove
Section titled “4. Pin the behaviour the local run cannot prove”The system host’s local run mocks the platform services with stateless OpenAPI mocks: the mocked idempotency service always answers “proceed”, so rerunning a seeded file locally reprocesses it. The local run proves your wiring; it cannot prove dedup. The test can, because FakeIdempotencyServiceClient lets you script the service’s answer:
files.AddFile("order-1.json", ValidOrderJson);idempotency.NextStatus = new StatusResponse(Action.Ignore, Reason.SameData);
var summary = await sweep.SweepAsync(CancellationToken.None);
Assert.Equal(1, summary.Cancelled); // a traceable no-op, not an errorAssert.Equal(0, topic.Count); // nothing publishedAssert.Empty(idempotency.Committed); // and no new commitAnything that depends on a platform service’s answer — duplicate cancellation, stale redeliveries, incident routing on a validation failure — belongs in a test like this, with the answer scripted. Add an idempotency check and Route business incidents cover choosing what those answers mean.
Verify
Section titled “Verify”Both suites green:
Passed! - Failed: 0, Passed: 7, Skipped: 0, Total: 7 - OrderExtractor.Test.Unit.dll (net10.0)Passed! - Failed: 0, Passed: 9, Skipped: 0, Total: 9 - OrderExtractor.Test.Integration.dll (net10.0)If it didn’t work, the failure’s location is the diagnosis:
- A unit test fails — the step’s own logic is wrong, or it returned the wrong result variant. Fix the step; nothing else is involved (step 2).
CompositionTestsfails — the production DI graph doesn’t build: a registration is missing or a framework upgrade renamed something. This failure would otherwise have surfaced when the system host started (step 3).- An integration test fails while its unit tests pass — the seam between steps is wrong: composition order, context keys, or an edge contract. Check what the fakes captured against what you expected (steps 3–4).
- Everything passes but the local run misbehaves — the difference is in what the run adds: Dapr components, binding names, the development definition. Start at the system host, not the tests.