Skip to content

Add sequenced responses for repeated request matches#133

Merged
dennisdoomen merged 2 commits into
mainfrom
dennisdoomen/sequenced-responses
Jun 27, 2026
Merged

Add sequenced responses for repeated request matches#133
dennisdoomen merged 2 commits into
mainfrom
dennisdoomen/sequenced-responses

Conversation

@dennisdoomen

Copy link
Copy Markdown
Owner

Closes #115

Summary

Adds support for configuring a sequence of responses for the same matched request, so consecutive calls get different responses (the classic "fail twice, then succeed" retry test, or paging). Chain Then(...) after any RespondsWith* method; the last response repeats for any calls beyond the configured sequence.

mock.ForGet().WithPath("/resource")
    .RespondsWithStatus(HttpStatusCode.ServiceUnavailable)
    .Then(HttpStatusCode.ServiceUnavailable)
    .Then(HttpStatusCode.OK);

Implementation

  • RequestMock now holds an ordered List<Func<RequestInfo, HttpResponseMessage>> of responders. The public Responder property is unchanged in type and continues to represent the first/only responder (backed by responders[0] under a lock), so existing get/set usage is preserved.
  • TrackRequest selects the responder by invocation index and repeats the last entry once the sequence is exhausted. A new internal AppendResponder adds to the list.
  • New internal ResponderFactory centralizes building of response funcs (status, JSON, OData, content, HTTP content), shared by both RespondsWith* and Then* to avoid duplication.
  • RequestMockBuilder leaf methods refactored onto a single CreateResponse(responder) helper.

New public API (additive, backward-compatible)

On RequestMockResponseBuilder:

  • Then(HttpStatusCode)
  • Then(Func<RequestInfo, HttpResponseMessage>)
  • Then(HttpContent) / Then(HttpStatusCode, HttpContent)
  • ThenRespondsWithJsonContent(...) (object / IResponseBuilder<T> / status-code overloads)
  • ThenRespondsWithContent(...)
  • ThenRespondsWithEmptyContent(...)
  • ThenRespondsWithODataResult(...) (entity / collection / builder / @odata.context overloads)

RequestMock.Responder is intentionally left as the canonical single/first responder (type unchanged) so existing usage keeps working.

Behavior notes

  • Sequence length and Times(n)/MaxInvocations are independent. With Times(2) and a 2-entry sequence, the mock stops matching after 2 invocations and a 3rd call throws UnexpectedRequestException.
  • The last configured response repeats for any subsequent matching calls.

Tests

Added a SequencedResponses spec class covering: sequence advances per call, last response repeats, single response still repeats, sequenced JSON bodies differ, combined with CollectingRequestsIn, combined with Times(n), appending a custom responder, and that Responder still exposes the first response.

Verification

  • dotnet test green on net8.0 (95) and net472 (94).
  • API verification tests regenerated via AcceptApiChanges.ps1; *.verified.txt updated and passing.
  • Analyzers (StyleCop, CSharpGuidelinesAnalyzer, Roslynator, Meziantou) pass with warnings-as-errors.

Do not merge — draft for maintainer review.

@dennisdoomen dennisdoomen added the enhancement New feature or request label May 30, 2026

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InspectCode found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown

Test Results

  3 files  ± 0    3 suites  ±0   7s ⏱️ -1s
193 tests +26  193 ✅ +26  0 💤 ±0  0 ❌ ±0 
383 runs  +52  383 ✅ +52  0 💤 ±0  0 ❌ ±0 

Results for commit cbd6a0f. ± Comparison against base commit fe4d7a7.

♻️ This comment has been updated with latest results.

@coveralls

coveralls commented May 30, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 28287943363

Coverage increased (+0.8%) to 84.659%

Details

  • Coverage increased (+0.8%) from the base build.
  • Patch coverage: 21 uncovered changes across 2 files (256 of 277 lines covered, 92.42%).
  • 5 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
Mockly/SequencedResponseBuilder.cs 112 99 88.39%
Mockly/RequestMock.cs 97 89 91.75%
Total (5 files) 277 256 92.42%

Coverage Regressions

5 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
Mockly/RequestMockResponseBuilder.cs 3 87.5%
Mockly/RequestMock.cs 2 86.75%

Coverage Stats

Coverage Status
Relevant Lines: 1476
Covered Lines: 1318
Line Coverage: 89.3%
Relevant Branches: 473
Covered Branches: 332
Branch Coverage: 70.19%
Branches in Coverage %: Yes
Coverage Strength: 330.34 hits per line

💛 - Coveralls

@dennisdoomen dennisdoomen force-pushed the dennisdoomen/sequenced-responses branch from eee4ca0 to 92f3fb1 Compare June 20, 2026 14:37
@dennisdoomen dennisdoomen marked this pull request as ready for review June 20, 2026 15:48
Comment thread Mockly/RequestMockResponseBuilder.cs Fixed
Comment thread Mockly.Specs/HttpMockSpecs.cs Fixed
Comment thread Mockly/RequestMockResponseBuilder.cs Fixed
Comment thread Mockly/RequestMockResponseBuilder.cs Fixed
Comment thread Mockly/RequestMockResponseBuilder.cs Fixed
Comment thread Mockly/RequestMockResponseBuilder.cs Fixed
Comment thread Mockly/RequestMock.cs Fixed
Comment thread Mockly.Specs/HttpMockSpecs.cs Fixed
Comment thread Mockly.Specs/HttpMockSpecs.cs Fixed
Comment thread Mockly.Specs/HttpMockSpecs.cs Fixed
dennisdoomen added a commit that referenced this pull request Jun 20, 2026
- UseCollectionExpression: replace new object[] with collection expression syntax in tests
- ParameterHidesMember: rename WithId parameter to avoid shadowing the field
- RedundantUsingDirective: guard System.Threading and System.Threading.Tasks under
  NET472_OR_GREATER since they are implicit global usings on net8.0
- RedundantArgumentDefaultValue: drop explicit 'application/json' passed to ThenRespondsWithContent
- PropertyCanBeMadeInitOnly / UnusedMember: add targeted ReSharper suppression comments on
  Responder setter and TrackRequest (both kept intentionally for public API compatibility)
- MemberCanBePrivate.Global / UnusedMethodReturnValue.Global: suppress via .editorconfig — public
  library methods are designed to be called externally; fluent return values are intentionally
  optional to use

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

#if NET472_OR_GREATER
using System.Net.Http;
using System.Threading;
#if NET472_OR_GREATER
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@dennisdoomen dennisdoomen changed the title Add sequenced/stateful responses (closes #115) Add sequenced/stateful responses Jun 21, 2026
Comment thread Mockly/RequestMock.cs Fixed
Comment thread Mockly/RequestMock.cs Fixed
Comment thread Mockly/RequestMockResponseBuilder.cs Fixed
using System.Text.Json;
#if NET472_OR_GREATER
using System.Net.Http;
using System.Threading;
#if NET472_OR_GREATER
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
/// </summary>
/// <param name="value">The entity to include in the OData result.</param>
public RequestMockResponseBuilder RespondsWithODataResult(object value)
public SequencedResponseBuilder RespondsWithODataResult(object value)
/// Once the last response's count (if set) is exhausted the mock stops matching; without an explicit
/// count the last response repeats indefinitely.
/// </remarks>
public SequencedResponseBuilder ThenRespondsWith(Func<RequestInfo, Task<HttpResponseMessage>> responder)
@dennisdoomen dennisdoomen force-pushed the dennisdoomen/sequenced-responses branch 2 times, most recently from 3b846bc to 01bee82 Compare June 27, 2026 10:48
dennisdoomen added a commit that referenced this pull request Jun 27, 2026
…ions

Replace ThenRespondsWithXXX_* method-prefixed test names with descriptive
behavioral names that follow the project's snake_case convention (e.g.
'An_appended_json_response_using_a_builder_produces_the_serialized_body').

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Redesign sequenced response API with SequencedResponseBuilder

- Introduce SequencedResponseBuilder as a subtype of RequestMockResponseBuilder
  so ThenXXX methods are only available on the result of RespondsWithXXX, not
  on any plain RequestMockResponseBuilder reference
- All RespondsWithXXX methods on RequestMockBuilder now return SequencedResponseBuilder
  (source-compatible: SequencedResponseBuilder IS-A RequestMockResponseBuilder)
- Once/Twice/Times are now per-response: each applies to the most recently
  configured response in the sequence, not to the mock as a whole
- Unify internal responder list as List<(Func<...> Responder, uint? Count)>
  so async responders participate in sequences via ThenXXX
- Remove AsyncResponder property; all responders go through the unified list
- Update A_sequence_combined_with_Times_stops_matching_after_the_limit test
  to reflect new per-response semantics (Times(2) on last response = 3 total calls)
- Update API approval snapshots

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Address InspectCode issues from PR #133

- UseCollectionExpression: replace new object[] with collection expression syntax in tests
- ParameterHidesMember: rename WithId parameter to avoid shadowing the field
- RedundantUsingDirective: guard System.Threading and System.Threading.Tasks under
  NET472_OR_GREATER since they are implicit global usings on net8.0
- RedundantArgumentDefaultValue: drop explicit 'application/json' passed to ThenRespondsWithContent
- PropertyCanBeMadeInitOnly / UnusedMember: add targeted ReSharper suppression comments on
  Responder setter and TrackRequest (both kept intentionally for public API compatibility)
- MemberCanBePrivate.Global / UnusedMethodReturnValue.Global: suppress via .editorconfig — public
  library methods are designed to be called externally; fluent return values are intentionally
  optional to use

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Add tests to cover all Then* sequenced response overloads

Cover Then(HttpContent), Then(HttpStatusCode, HttpContent),
ThenRespondsWithJsonContent<T>(IResponseBuilder<T>),
ThenRespondsWithJsonContent(HttpStatusCode, ...) (with builder),
ThenRespondsWithODataResult (single/collection/builder/context variants),
ThenRespondsWithContent and ThenRespondsWithEmptyContent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Add sequenced/stateful responses (.Then(...)) for consecutive calls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dennisdoomen dennisdoomen force-pushed the dennisdoomen/sequenced-responses branch from 01bee82 to 69e5c41 Compare June 27, 2026 10:50
dennisdoomen added a commit that referenced this pull request Jun 27, 2026
…ions

Replace ThenRespondsWithXXX_* method-prefixed test names with descriptive
behavioral names that follow the project's snake_case convention (e.g.
'An_appended_json_response_using_a_builder_produces_the_serialized_body').

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Redesign sequenced response API with SequencedResponseBuilder

- Introduce SequencedResponseBuilder as a subtype of RequestMockResponseBuilder
  so ThenXXX methods are only available on the result of RespondsWithXXX, not
  on any plain RequestMockResponseBuilder reference
- All RespondsWithXXX methods on RequestMockBuilder now return SequencedResponseBuilder
  (source-compatible: SequencedResponseBuilder IS-A RequestMockResponseBuilder)
- Once/Twice/Times are now per-response: each applies to the most recently
  configured response in the sequence, not to the mock as a whole
- Unify internal responder list as List<(Func<...> Responder, uint? Count)>
  so async responders participate in sequences via ThenXXX
- Remove AsyncResponder property; all responders go through the unified list
- Update A_sequence_combined_with_Times_stops_matching_after_the_limit test
  to reflect new per-response semantics (Times(2) on last response = 3 total calls)
- Update API approval snapshots

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Address InspectCode issues from PR #133

- UseCollectionExpression: replace new object[] with collection expression syntax in tests
- ParameterHidesMember: rename WithId parameter to avoid shadowing the field
- RedundantUsingDirective: guard System.Threading and System.Threading.Tasks under
  NET472_OR_GREATER since they are implicit global usings on net8.0
- RedundantArgumentDefaultValue: drop explicit 'application/json' passed to ThenRespondsWithContent
- PropertyCanBeMadeInitOnly / UnusedMember: add targeted ReSharper suppression comments on
  Responder setter and TrackRequest (both kept intentionally for public API compatibility)
- MemberCanBePrivate.Global / UnusedMethodReturnValue.Global: suppress via .editorconfig — public
  library methods are designed to be called externally; fluent return values are intentionally
  optional to use

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Add tests to cover all Then* sequenced response overloads

Cover Then(HttpContent), Then(HttpStatusCode, HttpContent),
ThenRespondsWithJsonContent<T>(IResponseBuilder<T>),
ThenRespondsWithJsonContent(HttpStatusCode, ...) (with builder),
ThenRespondsWithODataResult (single/collection/builder/context variants),
ThenRespondsWithContent and ThenRespondsWithEmptyContent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Add sequenced/stateful responses (.Then(...)) for consecutive calls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dennisdoomen dennisdoomen force-pushed the dennisdoomen/sequenced-responses branch from 69e5c41 to 7d17ec4 Compare June 27, 2026 11:16
dennisdoomen added a commit that referenced this pull request Jun 27, 2026
…ions

Replace ThenRespondsWithXXX_* method-prefixed test names with descriptive
behavioral names that follow the project's snake_case convention (e.g.
'An_appended_json_response_using_a_builder_produces_the_serialized_body').

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Redesign sequenced response API with SequencedResponseBuilder

- Introduce SequencedResponseBuilder as a subtype of RequestMockResponseBuilder
  so ThenXXX methods are only available on the result of RespondsWithXXX, not
  on any plain RequestMockResponseBuilder reference
- All RespondsWithXXX methods on RequestMockBuilder now return SequencedResponseBuilder
  (source-compatible: SequencedResponseBuilder IS-A RequestMockResponseBuilder)
- Once/Twice/Times are now per-response: each applies to the most recently
  configured response in the sequence, not to the mock as a whole
- Unify internal responder list as List<(Func<...> Responder, uint? Count)>
  so async responders participate in sequences via ThenXXX
- Remove AsyncResponder property; all responders go through the unified list
- Update A_sequence_combined_with_Times_stops_matching_after_the_limit test
  to reflect new per-response semantics (Times(2) on last response = 3 total calls)
- Update API approval snapshots

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Address InspectCode issues from PR #133

- UseCollectionExpression: replace new object[] with collection expression syntax in tests
- ParameterHidesMember: rename WithId parameter to avoid shadowing the field
- RedundantUsingDirective: guard System.Threading and System.Threading.Tasks under
  NET472_OR_GREATER since they are implicit global usings on net8.0
- RedundantArgumentDefaultValue: drop explicit 'application/json' passed to ThenRespondsWithContent
- PropertyCanBeMadeInitOnly / UnusedMember: add targeted ReSharper suppression comments on
  Responder setter and TrackRequest (both kept intentionally for public API compatibility)
- MemberCanBePrivate.Global / UnusedMethodReturnValue.Global: suppress via .editorconfig — public
  library methods are designed to be called externally; fluent return values are intentionally
  optional to use

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Add tests to cover all Then* sequenced response overloads

Cover Then(HttpContent), Then(HttpStatusCode, HttpContent),
ThenRespondsWithJsonContent<T>(IResponseBuilder<T>),
ThenRespondsWithJsonContent(HttpStatusCode, ...) (with builder),
ThenRespondsWithODataResult (single/collection/builder/context variants),
ThenRespondsWithContent and ThenRespondsWithEmptyContent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Add sequenced/stateful responses (.Then(...)) for consecutive calls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dennisdoomen dennisdoomen force-pushed the dennisdoomen/sequenced-responses branch from 7d17ec4 to 25366a3 Compare June 27, 2026 11:17
@dennisdoomen dennisdoomen changed the title Add sequenced/stateful responses Add sequenced responses for repeated request matches Jun 27, 2026
@dennisdoomen dennisdoomen force-pushed the dennisdoomen/sequenced-responses branch from 25366a3 to 7ee5fc3 Compare June 27, 2026 11:24
Keep the synchronous request tracking entry point and preserve the public setter on the responder property so API approval checks remain stable while using the new async sequencing internals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread Mockly/RequestMock.cs
/// This synchronous API is preserved for backward compatibility. Internally it delegates
/// to the asynchronous execution path without a cancellation token.
/// </remarks>
public CapturedRequest TrackRequest(RequestInfo request)
@dennisdoomen dennisdoomen merged commit 7acc4f8 into main Jun 27, 2026
10 checks passed
@dennisdoomen dennisdoomen deleted the dennisdoomen/sequenced-responses branch June 27, 2026 15:46
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.

Add sequenced/stateful responses (.Then(...)) for consecutive calls

3 participants