NResilience.Testing
0.33.0
dotnet add package NResilience.Testing --version 0.33.0
NuGet\Install-Package NResilience.Testing -Version 0.33.0
<PackageReference Include="NResilience.Testing" Version="0.33.0" />
<PackageVersion Include="NResilience.Testing" Version="0.33.0" />
<PackageReference Include="NResilience.Testing" />
paket add NResilience.Testing --version 0.33.0
#r "nuget: NResilience.Testing, 0.33.0"
#:package NResilience.Testing@0.33.0
#addin nuget:?package=NResilience.Testing&version=0.33.0
#tool nuget:?package=NResilience.Testing&version=0.33.0
NResilience.Testing
Test helpers for NResilience: scripted callbacks, a recording telemetry listener, fake-time support, and a deterministic simulator, for fast tests that measure what a policy actually costs.
Install
Install the package using the .NET CLI:
dotnet add package NResilience.Testing
Why a separate package
Testing retries and timeouts against the real clock is slow and flaky - a 30-second timeout takes 30 seconds to test, and timing varies across machines.
NResilience.Testing lets you script dependency behavior, capture policy events for assertion, and advance time manually, so a timeout test runs in
microseconds.
It is a test-time dependency only and does not affect the core library in production.
Script the callback
Sequence<T> serves a script of returns, throws, and delays one by one as the policy makes attempts:
Sequence<HttpResponseMessage> calls = Sequence.For<HttpResponseMessage>()
.Returns(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), count: 2)
.Returns(new HttpResponseMessage(HttpStatusCode.OK));
var policy = Resilience.Http with { Backoff = Backoff.None };
CallResult<HttpResponseMessage> result = await policy.TryRunAsync(attempt => calls.NextAsync(attempt));
Assert.True(result.IsSuccess);
Assert.Equal(3, calls.CallCount);
Assert.Equal(3, result.Attempts.Count);
Control the clock
Pass the same FakeTimeProvider to both the policy and the sequence to test timeouts without waiting:
var time = new FakeTimeProvider();
Sequence<int> calls = Sequence.For<int>(time)
.Delays(TimeSpan.FromSeconds(30)) // longer than the attempt timeout
.Returns(1);
var policy = Resilience.Default with
{
Time = time,
Attempts = 1,
AttemptTimeout = TimeSpan.FromSeconds(3),
};
Task<CallResult<int>> pending = policy.TryRunAsync(attempt => calls.NextAsync(attempt)).AsTask();
time.Advance(TimeSpan.FromSeconds(4));
CallResult<int> result = await pending;
Assert.IsType<AttemptTimeoutException>(result.Exception);
Pass the same TimeProvider to both the policy and the sequence. If the sequence uses the system clock while the policy uses a fake clock, the scripted delay
becomes a real sleep.
Verify policy behavior
EventRecorder captures every CallEvent in order, so you can assert on the sequence rather than elapsed time:
var events = new EventRecorder();
Sequence<int> calls = Sequence.For<int>().Throws(new IOException()).Returns(42);
var policy = Resilience.Default with { Backoff = Backoff.None, OnEvent = events.Record };
await policy.RunAsync(attempt => calls.NextAsync(attempt));
Assert.Equal(
[CallEventKind.Attempt, CallEventKind.Retrying, CallEventKind.Attempt, CallEventKind.Succeeded],
events.Kinds);
Reach for a ready-made policy
TestPolicy.Instant is a Resilience value shaped for tests: three attempts, no backoff, and both the deadline and the attempt timeout set to infinite, so a
test pays for neither a sleep nor a wall-clock bound it does not care about. TestPolicy.InstantHttp is the same shape with Classifier = Classifier.Http.
var api = TestPolicy.Instant;
To run Instant on a FakeTimeProvider, call TestPolicy.WithClock(time). It rebuilds any breaker the policy carries on that same clock, so the policy, its breaker
and its budget all advance together:
var time = new FakeTimeProvider();
var api = TestPolicy.WithClock(time);
Test an HTTP client
Provide a scripted HttpMessageHandler as the inner handler to test a resilient HttpClient end to end. ScriptedHttpHandler serves the script you give it,
then repeats the last step for every attempt after that:
var transport = new ScriptedHttpHandler()
.Responds(HttpStatusCode.ServiceUnavailable)
.Responds(HttpStatusCode.OK);
using HttpClient client = HttpResilience.CreateClient(
Resilience.Http with { Backoff = Backoff.None },
innerHandler: transport);
using HttpResponseMessage response = await client.GetAsync(new Uri("https://api.example.com/orders/1"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(2, transport.CallCount);
Respond(status) and Respond(status, times) serve a fixed status code, once or for a run of attempts. Respond(response) and Respond(response, times)
build a fresh HttpResponseMessage per attempt, for a response whose content a test reads. Throw(exception) and Throw(exception, times) throw instead, for
the transport failures a classifier has to see.
CallCount is how many attempts reached the handler. Requests is a snapshot of what each attempt sent, in order: the method, the URI, the headers, and - only
when CaptureBodies is true - the body.
Simulate a whole configuration
Simulate runs your real policy against a modeled dependency on a virtual clock and reports what it cost. Nothing sleeps, so five simulated minutes cost about
what a unit test costs, and the same seed produces the same report on any machine:
SimulationReport report = Simulate.Policy(api)
.Against(Dependency
.Healthy(p50: TimeSpan.FromMilliseconds(20), p99: TimeSpan.FromMilliseconds(200))
.Brownout(after: TimeSpan.FromSeconds(30), slower: 8, lasting: TimeSpan.FromMinutes(1)))
.Under(Load.Constant(perSecond: 500))
.For(TimeSpan.FromMinutes(5))
.Run(seed: 42);
Assert.True(report.LoadMultiplier <= 1.2); // attempts that reached the dependency, per call you made
Assert.True(report.Availability >= 0.9);
LoadMultiplier, Amplification, Availability, Latency(quantile), BreakerOpens, and TimeToRecover are the measurements; CountOf(kind) reaches the
rest of the telemetry. Only the clock, the random source, and the dependency are modeled - the executor, the breaker, the retry budget, the classifier, and
every estimator are the shipping ones.
Documentation
For more information, see the following resources:
- Testing guide - the full walkthrough, including best practices for keeping tests fast and deterministic.
- Simulation - modeling a dependency, offering load, and reading the report.
Feedback
Provide feedback using these channels:
- Usage questions
- Bug reports and feature requests
- Security vulnerabilities - private advisory, not a public issue.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- NResilience (>= 0.33.0)
- System.Threading.RateLimiting (>= 10.0.11)
-
net8.0
- NResilience (>= 0.33.0)
- System.Threading.RateLimiting (>= 10.0.11)
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 |
|---|---|---|
| 0.33.0 | 48 | 9/23/2026 |
| 0.32.0 | 94 | 9/10/2026 |
| 0.31.0 | 95 | 9/9/2026 |
| 0.30.0 | 97 | 9/9/2026 |
| 0.29.0 | 104 | 9/9/2026 |
| 0.28.0 | 98 | 9/9/2026 |
| 0.27.0 | 93 | 9/9/2026 |
| 0.26.0 | 99 | 9/8/2026 |
| 0.25.0 | 102 | 9/8/2026 |
| 0.24.0 | 97 | 9/8/2026 |
| 0.23.0 | 100 | 9/5/2026 |
| 0.22.0 | 105 | 9/5/2026 |
| 0.21.0 | 108 | 9/3/2026 |
| 0.20.0 | 96 | 9/3/2026 |
| 0.19.0 | 99 | 9/3/2026 |
| 0.18.0 | 100 | 9/1/2026 |
| 0.17.0 | 97 | 9/1/2026 |
| 0.16.0 | 98 | 9/1/2026 |
| 0.15.0 | 91 | 9/1/2026 |
| 0.14.0 | 197 | 8/30/2026 |