Intropy.Framework.Testing 1.1.0-beta.4

This is a prerelease version of Intropy.Framework.Testing.
dotnet add package Intropy.Framework.Testing --version 1.1.0-beta.4
                    
NuGet\Install-Package Intropy.Framework.Testing -Version 1.1.0-beta.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Intropy.Framework.Testing" Version="1.1.0-beta.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Intropy.Framework.Testing" Version="1.1.0-beta.4" />
                    
Directory.Packages.props
<PackageReference Include="Intropy.Framework.Testing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Intropy.Framework.Testing --version 1.1.0-beta.4
                    
#r "nuget: Intropy.Framework.Testing, 1.1.0-beta.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Intropy.Framework.Testing@1.1.0-beta.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Intropy.Framework.Testing&version=1.1.0-beta.4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Intropy.Framework.Testing&version=1.1.0-beta.4&prerelease
                    
Install as a Cake Tool

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

  1. Build the component's pipeline/host exactly as production composition does — same steps, same builder calls.
  2. Fake the edges:
    • Extractors: ExtractorBuilder<...>.WithSender(fakeTopic) in place of WithDaprTopicPublisher(...).
    • Loaders: override the keyed IFileAdapter registration with an InMemoryFileAdapter, and the two service clients with their fakes (plain DI override — last registration wins).
  3. 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 throw FileNotFoundException (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/fileName when a write passes an override). Pass a basePath to the constructor to make ListAsync mirror LocalFileAdapter: only files under that path, listed by file name. ReadException / WriteException / DeleteException simulate a dead source/destination. Per-file faults: AddUnreadableFile(name) seeds a file that is listed but throws on read (corrupt source file), and SetDeleteException(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 exact CloudEvent instances the real publisher would have encoded. SendException surfaces 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 via ReceivePipelineBuilder<TCtx>.WithEnqueuer(fake). Captures each SourceItem and its already-encoded structured-mode CloudEvents envelope (defensive byte[] copies — the formatter's buffer is recycled); decode via CapturedEnqueue.DecodeCloudEvent(). Register it as the EnqueueStep<TCtx> to replace the built-in DaprTopicEnqueuer: the receive pipeline AddTransactionalIntegration registers publishes through it. SendException is 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 for BusinessIncidentServiceException; any other exception type takes a different code path than production.

  • DaprDelivery.DeliverAsync — POSTs the structured-mode envelope as application/cloudevents+json and parses the {"status": "..."} ack. Unknown, missing, or malformed statuses map to DeliveryAck.Retry, matching the sidecar's fail-safe redelivery. DeliveryAck members map to wire values as Success ↔ SUCCESS, Retry ↔ RETRY, Drop ↔ DROP.

  • PublishedMessageCapture — records publish calls made through a DaprClient substitute configured by your test project (works with any mocking framework; one wiring line per fake). Assert on the captured PublishedMessages and decode envelopes via DecodeCloudEvent():

    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 empty CloudEvent, never null. Do not assert on null result values; assert on Incidents instead.

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, ...) are volatile — safe to toggle between runs, not a coordination primitive for mid-run assertions.
  • Versioned in lockstep with the framework packages.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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 35 9/29/2026
1.1.0-beta.3 33 9/29/2026
1.1.0-beta.2 35 9/29/2026
1.1.0-beta.1 43 9/28/2026
1.0.0-beta.2 90 9/9/2026
1.0.0-beta.1 71 9/8/2026
0.3.0-beta.5 81 8/12/2026
0.3.0-beta.1 84 8/11/2026