Intropy.Framework.Testing
1.1.0-beta.4
dotnet add package Intropy.Framework.Testing --version 1.1.0-beta.4
NuGet\Install-Package Intropy.Framework.Testing -Version 1.1.0-beta.4
<PackageReference Include="Intropy.Framework.Testing" Version="1.1.0-beta.4" />
<PackageVersion Include="Intropy.Framework.Testing" Version="1.1.0-beta.4" />
<PackageReference Include="Intropy.Framework.Testing" />
paket add Intropy.Framework.Testing --version 1.1.0-beta.4
#r "nuget: Intropy.Framework.Testing, 1.1.0-beta.4"
#:package Intropy.Framework.Testing@1.1.0-beta.4
#addin nuget:?package=Intropy.Framework.Testing&version=1.1.0-beta.4&prerelease
#tool nuget:?package=Intropy.Framework.Testing&version=1.1.0-beta.4&prerelease
Intropy.Framework.Testing
Hand-rolled fakes and delivery helpers for component integration tests against
the Intropy framework.
No mocking-framework dependency, no Dapr sidecar required. It depends on
Intropy.Framework.Hosting (for the loader delivery fakes), and so on the ASP.NET
Core shared framework, like every component does.
The package fakes the four edges every component integration test fakes, plus helpers for delivering CloudEvents to loaders exactly as a Dapr sidecar does:
| Helper | Namespace | Fakes |
|---|---|---|
InMemoryFileAdapter |
Intropy.Framework.Testing.Adapters |
IFileAdapter (source/destination connectors) |
FakeTopic<TCtx> |
Intropy.Framework.Testing.Topics |
The extractor's Dapr pub/sub publish step |
FakeEnqueueStep<TCtx> |
Intropy.Framework.Testing.Topics |
The transactional receive pipeline's queue publish step |
FakeIdempotencyServiceClient |
Intropy.Framework.Testing.Services |
IIdempotencyServiceClient |
FakeBusinessIncidentServiceClient |
Intropy.Framework.Testing.Services |
IBusinessIncidentServiceClient |
FakeStreamingSubscriber |
Intropy.Framework.Testing.Delivery |
The sidecar's streaming subscription, for loaders with message routes (AddLoader) |
AppCallbackDelivery |
Intropy.Framework.Testing.Delivery |
The sidecar calling a batch loader's gRPC app callback (OnBatch, AddBatchLoader) |
DaprDelivery |
Intropy.Framework.Testing.Delivery |
Sidecar HTTP delivery to ASP.NET subscription endpoints |
PublishedMessageCapture |
Intropy.Framework.Testing.Dapr |
Publish-call capture for DaprClient substitutes |
The canonical pattern
- Build the component's pipeline/host exactly as production composition does — same steps, same builder calls.
- Fake the edges:
- Extractors:
ExtractorBuilder<...>.WithSender(fakeTopic)in place ofWithDaprTopicPublisher(...). - Loaders: override the keyed
IFileAdapterregistration with anInMemoryFileAdapter, and the two service clients with their fakes (plain DI override — last registration wins).
- Extractors:
- Assert on fake state: topic count/events, adapter files, recorded incidents/resolutions and commits, delivery acks.
// Extractor test shape
var topic = new FakeTopic<MyContext>();
var pipeline = MyExtractorComposition.BuildPipeline(
extractorBuilder => extractorBuilder.WithSender(topic));
await RunSweep(pipeline, fileAdapter); // component's own orchestration
Assert.Equal(2, topic.Count);
Assert.Equal("order.created", topic.Events[0].Type);
// Loader test shape (message routes: the host consumes through a streaming subscription)
var subscriber = new FakeStreamingSubscriber();
var builder = Composition.CreateHostBuilder([], "Development");
builder.Services.AddSingleton<IStreamingSubscriber>(subscriber); // plus the edge fakes
using var host = builder.Build();
await host.StartAsync();
await subscriber.WaitForSubscriptionAsync();
var ack = await subscriber.DeliverAsync(cloudEvent); // redelivery: true marks a retry
Assert.Equal(DeliveryAck.Success, ack);
Assert.Equal(expectedJson, destinationFiles.GetString("out/order-42.json"));
// Batch loader test shape (batch routes: the host serves the Dapr gRPC app callback)
var port = AppCallbackDelivery.AvailablePort();
// ... build and start the host with LoaderOptions.CallbackPort = port, plus the edge fakes ...
using var delivery = new AppCallbackDelivery(port);
var subscription = Assert.Single(await delivery.GetSubscriptionsAsync()); // the announced bulk settings
var acks = await delivery.DeliverBatchAsync(created1, created2, cancelled1); // one ack per entry, in order
Assert.All(acks, ack => Assert.Equal(DeliveryAck.Success, ack));
Both loader fakes encode events the way the sidecar hands them over — the
payload as the envelope's data member (a payload published as a JSON string
arrives quoted, as it does for real), every other attribute as an extension — and
return the loader's ack as a DeliveryAck. DaprDelivery is for ASP.NET
components that still receive over HTTP delivery.
Fake semantics (fail like production)
InMemoryFileAdapter— missing reads throwFileNotFoundException(the Dapr binding throws; it never returns null); deletes of missing files no-op (binding delete is idempotent); files are keyed on the effective path (basePath/fileNamewhen a write passes an override). Pass abasePathto the constructor to makeListAsyncmirrorLocalFileAdapter: only files under that path, listed by file name.ReadException/WriteException/DeleteExceptionsimulate a dead source/destination. Per-file faults:AddUnreadableFile(name)seeds a file that is listed but throws on read (corrupt source file), andSetDeleteException(name, ex)makes deletes fail for one file while others succeed — the publish-succeeds-but-delete-fails path, where the file is re-processed next run and idempotency must catch it.FakeTopic<TCtx>— captures the exactCloudEventinstances the real publisher would have encoded.SendExceptionsurfaces as a technical failure through the framework's normal exception handling, matching a dead broker.FakeEnqueueStep<TCtx>— the transactional receive pipeline's enqueue seam, plugged in viaReceivePipelineBuilder<TCtx>.WithEnqueuer(fake). Captures eachSourceItemand its already-encoded structured-mode CloudEvents envelope (defensivebyte[]copies — the formatter's buffer is recycled); decode viaCapturedEnqueue.DecodeCloudEvent(). Register it as theEnqueueStep<TCtx>to replace the built-inDaprTopicEnqueuer: the receive pipelineAddTransactionalIntegrationregisters publishes through it.SendExceptionis thrown before capture, surfaces as a technical failure, and — because the sweep completes a file only after it is enqueued — leaves the source file undeleted, matching a dead broker in production.Service fakes — faults throw the typed
IdempotencyServiceException/BusinessIncidentServiceException. Typing matters: the framework's incident router has a dedicated catch forBusinessIncidentServiceException; any other exception type takes a different code path than production.DaprDelivery.DeliverAsync— POSTs the structured-mode envelope asapplication/cloudevents+jsonand parses the{"status": "..."}ack. Unknown, missing, or malformed statuses map toDeliveryAck.Retry, matching the sidecar's fail-safe redelivery.DeliveryAckmembers map to wire values asSuccess↔SUCCESS,Retry↔RETRY,Drop↔DROP.PublishedMessageCapture— records publish calls made through aDaprClientsubstitute configured by your test project (works with any mocking framework; one wiring line per fake). Assert on the capturedPublishedMessages and decode envelopes viaDecodeCloudEvent():var daprClient = Substitute.For<DaprClient>(); var capture = new PublishedMessageCapture(); daprClient .PublishByteEventAsync( Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ReadOnlyMemory<byte>>(), Arg.Any<string?>(), Arg.Any<Dictionary<string, string>?>(), Arg.Any<CancellationToken>()) .Returns(Task.CompletedTask) .AndDoes(ci => capture.Capture( ci.Arg<string>(), ci.ArgAt<string>(1), ci.ArgAt<ReadOnlyMemory<byte>>(2), ci.ArgAt<string?>(3))); // ... run the pipeline ... var message = capture.Messages.Single(); Assert.Equal("orders", message.TopicName); Assert.Equal("order-42", message.DecodeCloudEvent().Subject);
Loader ack/consumption matrix
This matrix describes the behavior of framework-composed loader and transactional pipelines (which acks their endpoints/handlers produce), not of this package. The delivery helpers only deliver the event and report the ack.
| Scenario | Ack | Side effects |
|---|---|---|
| Valid message | SUCCESS |
File written to destination |
Duplicate (idempotency Ignore) |
SUCCESS |
Nothing written |
| Business-rule violation | SUCCESS |
Incident routed — consumed, never retried |
| Destination throws | RETRY |
Nothing written |
Idempotency service down (StatusException) |
RETRY |
Technical failure |
| Malformed envelope | RETRY |
No incident routed |
| No route for the event type (routing loader) | DROP (default UnroutedPolicy.DeadLetter) |
Sent to the dead-letter topic |
Filtered out by a batch route's Where |
SUCCESS |
Nothing looked up or written |
| Batch lookup fails | RETRY for that chunk's entries |
Other chunks and message routes unaffected |
Business failures are consumed. When a business step fails, the framework's incident-router finalizer triggers the incident and returns
Success(defaultValueFactory())— for framework-wired extractors an emptyCloudEvent, never null. Do not assert on null result values; assert onIncidentsinstead.
Extractor sweep matrix
| Scenario | Published | Source file |
|---|---|---|
| Valid | Yes | Deleted |
Duplicate (idempotency Ignore) |
No | Still deleted |
| Validation failure | Incident routed | Consumed (empty-CloudEvent success) |
Technical failure (SendException / ReadException) |
No | Left for the next run |
Transactional receive side matrix
Per source file, swept by FileSweep around the receive pipeline:
| Scenario | Enqueued | Source file |
|---|---|---|
| Valid | Yes | Deleted (or archived) after the enqueue |
| Unreadable source (adapter read throws) | No | Left for the next run; counted as failed |
Broker down (SendException set) |
No — technical failure | Left for the next run; counted as failed |
Enqueue OK, delete throws (SetDeleteException) |
Yes — the duplicate lands on the queue | Left; re-processed next run, send-side idempotency must absorb it |
The last row is the most valuable one: the enqueue is captured, completion fails, and the file is re-processed next run.
Transactional send pipeline matrix
Per delivered message:
| Scenario | Written | Idempotency |
|---|---|---|
| Valid | Yes | Committed |
Duplicate (idempotency Ignore) |
No | No new commit |
| Validation failure | No — incident routed, consumed | No commit |
| Destination throws | No — business failure, incident routed² | No commit |
² Transactional SendStep is a BusinessStep; an uncaught exception routes an
incident, it is not a technical failure. This is why this package ships no
FakeSendStep for the send pipeline: a FakeTopic-style SendException knob
would surface as a business incident and mis-model production. Assert through
the component's real sender + InMemoryFileAdapter instead — it tests more,
including the file-naming convention.
Notes
- Thread-safe: all recorded state is lock-guarded and exposed as snapshots, so
parallel extractor sweeps cannot corrupt assertions. Fault knobs
(
ReadException,SendException, ...) arevolatile— safe to toggle between runs, not a coordination primitive for mid-run assertions. - Versioned in lockstep with the framework packages.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Dapr.Client (>= 1.18.10)
- Grpc.Net.Client (>= 2.84.0)
- Intropy.Contracts (>= 0.1.0)
- Intropy.Framework.Adapters (>= 1.1.0-beta.4)
- Intropy.Framework.Blocks (>= 1.1.0-beta.4)
- Intropy.Framework.Hosting (>= 1.1.0-beta.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0-beta.4 | 36 | 9/29/2026 |
| 1.1.0-beta.3 | 34 | 9/29/2026 |
| 1.1.0-beta.2 | 36 | 9/29/2026 |
| 1.1.0-beta.1 | 44 | 9/28/2026 |
| 1.0.0-beta.2 | 92 | 9/9/2026 |
| 1.0.0-beta.1 | 71 | 9/8/2026 |
| 0.3.0-beta.5 | 82 | 8/12/2026 |
| 0.3.0-beta.1 | 84 | 8/11/2026 |