Skip to content

Add simulated network failures ThrowsException/TimesOut (closes #116)#136

Draft
dennisdoomen wants to merge 1 commit into
dennisdoomen/response-latency-afterfrom
dennisdoomen/simulated-network-failures
Draft

Add simulated network failures ThrowsException/TimesOut (closes #116)#136
dennisdoomen wants to merge 1 commit into
dennisdoomen/response-latency-afterfrom
dennisdoomen/simulated-network-failures

Conversation

@dennisdoomen

Copy link
Copy Markdown
Owner

Closes #116

Stacking

This is the top of a 3-PR stack and targets dennisdoomen/response-latency-after:

It builds on the async responder path (TrackRequestAsync, CancellationToken flowing from SendAsyncHandleRequestRequestMock) introduced in those PRs and does not re-implement them.

Summary

Adds first-class support for simulating network-level failures so tests can exercise retry, circuit-breaker and error-handling code.

Previously the pipeline swallowed every responder exception and turned it into a 500 response, so there was no way to simulate a thrown transport error. This change introduces a dedicated simulated-failure responder that the async pipeline propagates to the HttpClient caller instead of catching:

  • A new internal SimulatedFailureResponder type represents an intentionally simulated failure.
  • RequestMock.TrackRequestAsync records the request (incrementing the invocation count and adding it to any request collection) and returns the captured request carrying the exception, rather than throwing inline. This guarantees the failed call is recorded in HttpMock.Requests (as expected/handled) before the exception reaches the caller.
  • HttpMock.HandleRequest rethrows the captured exception via ExceptionDispatchInfo after recording it.
  • The existing try/catch that converts responder exceptions into a 500 is carefully scoped so genuine bugs in user RespondsWith responders keep the current 500 behavior, while intentionally-simulated failures propagate.
  • Once() / Times(n) and After() continue to apply to simulated failures.

New public API

RequestMockResponseBuilder ThrowsException(Exception exception);
RequestMockResponseBuilder ThrowsException<TException>() where TException : Exception, new();
RequestMockResponseBuilder TimesOut(); // throws TaskCanceledException, mimicking an HttpClient timeout

Plus CapturedRequest.SimulatedFailure (Exception?) so a captured entry can be inspected to tell a simulated throw apart from an unexpected 500.

Usage:

mock.ForGet().WithPath("/flaky").ThrowsException<HttpRequestException>();
mock.ForGet().WithPath("/slow").TimesOut();

This is additive and backward-compatible. The API verification baselines (net8.0, net472) were regenerated via AcceptApiChanges.ps1.

Tests

New WhenSimulatingNetworkFailures spec class (xUnit + FluentAssertions, AAA) covering:

  • HttpRequestException surfaces to the HttpClient caller (generic and specific-instance overloads)
  • TimesOut() throws TaskCanceledException
  • the failed call is captured in HttpMock.Requests and in a CollectingRequestsIn collection
  • Once() / Times(n) still apply
  • a genuinely buggy responder still results in a 500
  • null exception is rejected

All 108 specs pass on both net8.0 and net472; Release build (warnings-as-errors, all analyzers) is clean.

Follow-up

Issue #116 also describes a companion CapturedRequestAssertions.BeASimulatedFailure() assertion in the separate dennisdoomen/fluentassertions.mockly repo (v7/v8 via Shared/). That is not part of this repo/workspace and needs its own PR; the new public CapturedRequest.SimulatedFailure property is exposed here so that assertion can be built on top of it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dennisdoomen dennisdoomen added the enhancement New feature or request label May 30, 2026
}

[Fact]
public async Task A_null_exception_is_rejected()
/// </remarks>
/// <param name="exception">The exception to throw for a matching request.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="exception"/> is <c>null</c>.</exception>
public RequestMockResponseBuilder ThrowsException(Exception exception)
/// configured timeout. The exception is propagated to the caller and the matching request is still recorded in
/// <see cref="HttpMock.Requests"/> before the exception is thrown.
/// </remarks>
public RequestMockResponseBuilder TimesOut()
{
private readonly Func<Exception> exceptionFactory;

public SimulatedFailureResponder(Func<Exception> exceptionFactory)
@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown

Test Results

  3 files  ± 0    3 suites  ±0   13s ⏱️ +5s
110 tests + 9  110 ✅ + 9  0 💤 ±0  0 ❌ ±0 
217 runs  +18  217 ✅ +18  0 💤 ±0  0 ❌ ±0 

Results for commit 2ca9d81. ± Comparison against base commit a2b689e.

♻️ This comment has been updated with latest results.

@coveralls

coveralls commented May 30, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 26678769048

Coverage increased (+0.2%) to 82.82%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 5 uncovered changes across 1 file (50 of 55 lines covered, 90.91%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
Mockly/RequestMock.cs 19 14 73.68%
Total (5 files) 55 50 90.91%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1124
Covered Lines: 985
Line Coverage: 87.63%
Relevant Branches: 337
Covered Branches: 225
Branch Coverage: 66.77%
Branches in Coverage %: Yes
Coverage Strength: 338.44 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants