Intropy.Framework.Testing 2.1.0-beta.5

This is a prerelease version of Intropy.Framework.Testing.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Intropy.Framework.Testing --version 2.1.0-beta.5
                    
NuGet\Install-Package Intropy.Framework.Testing -Version 2.1.0-beta.5
                    
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="2.1.0-beta.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Intropy.Framework.Testing" Version="2.1.0-beta.5" />
                    
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 2.1.0-beta.5
                    
#r "nuget: Intropy.Framework.Testing, 2.1.0-beta.5"
                    
#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@2.1.0-beta.5
                    
#: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=2.1.0-beta.5&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Intropy.Framework.Testing&version=2.1.0-beta.5&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 helpers).

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
AppCallbackDelivery Intropy.Framework.Testing.Delivery The sidecar's pushes to a subscribing block's gRPC app callback: loaders (AddLoader) and transactional integrations
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). A pipeline without WithIdempotency never resolves IIdempotencyServiceClient, so its fake is only needed when the pipeline configures idempotency.
  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: the loader serves the Dapr gRPC app callback on a port, and the
// delivery plays the sidecar over it.
var port = AppCallbackDelivery.AvailablePort();
builder.Services.AddLoader(options =>
{
    options.PubSubName = "pubsub";
    options.TopicName = "orders";
    options.CallbackPort = port;
}, routes => /* the component's routes */);                          // plus the edge fakes
using var host = builder.Build();
await host.StartAsync();
using var delivery = new AppCallbackDelivery(port, "pubsub", "orders");

var ack = await delivery.DeliverAsync(cloudEvent);                  // redelivery: true marks a retry

Assert.Equal(DeliveryAck.Success, ack);
Assert.Equal(expectedJson, destinationFiles.GetString("out/order-42.json"));

A transactional integration serves the same callback for the length of a run (TransactionalIntegrationOptions.CallbackPort): deliver to it while the run's sweep is still going, and it consumes until its queue goes idle.

AppCallbackDelivery encodes events the way the sidecar hands them over — the payload as the envelope's data member (an object payload as a camelCase JSON object, as the framework's blocks publish it; a string payload arrives quoted, as it does for real), every other attribute as an extension — and returns the block's ack as a DeliveryAck. A block never answers DROP: what it cannot process it leaves for redelivery (RETRY), so the broker dead-letters it.

A subscribing block announces no subscription (a declarative Dapr Subscription resource owns it), so AppCallbackDelivery is told the pub/sub and topic it delivers on, as the resource tells the sidecar. An overload of DeliverAsync delivers from another pub/sub or topic, to test a resource that disagrees with the block. 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; only with WithIdempotency) 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) RETRY (default UnroutedPolicy.DeadLetter) Handed back to the broker, which dead-letters it
Processing exceeds MaxMessageProcessingTime RETRY Pipeline cancelled
Delivered after the host began stopping RETRY Not processed
Delivered from another pub/sub or topic (app callback) RETRY Not processed; error logged

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; only with WithIdempotency) 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 — incident routed, message left for redelivery² No commit

² Transactional SendStep is a BusinessStep, so an uncaught exception routes an incident rather than becoming a technical failure — but it is retriable, so the message is left for redelivery rather than acknowledged. A send step that wants the message consumed says so: Failure(incident, Retriable: false).

This package still ships no FakeSendStep for the send pipeline. 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
2.1.0-beta.6 0 10/23/2026
2.1.0-beta.5 0 10/23/2026
2.1.0-beta.4 36 10/22/2026
2.1.0-beta.3 38 10/21/2026
2.1.0-beta.2 56 10/19/2026
2.1.0-beta.1 51 10/19/2026
2.0.1-beta.1 48 10/16/2026
2.0.0-beta.1 56 10/15/2026
1.2.1-beta.5 50 10/14/2026
1.2.1-beta.4 51 10/13/2026
1.2.1-beta.2 54 10/13/2026
1.2.1-beta.1 48 10/13/2026
1.2.0-beta.3 53 10/8/2026
1.2.0-beta.2 58 10/8/2026
1.2.0-beta.1 57 10/7/2026
1.1.0-beta.4 74 9/29/2026
1.1.0-beta.3 65 9/29/2026
1.1.0-beta.2 65 9/29/2026
1.1.0-beta.1 64 9/28/2026
1.0.0-beta.2 104 9/9/2026
Loading failed