Skip to content

Commit a0c96cf

Browse files
authored
Introduce ClearRequests method to reset the handler (#195)
* Add ClearRequests method to partly reset the TestableHttpMessageHandler * Make sure SequencedResponse can recover from resets. Fixes #193
1 parent 1115873 commit a0c96cf

19 files changed

+196
-183
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1515
- `TestableHttpMessageHandler.RespondWith(Func<HttpRequestMessage, HttpResponseMessage>)` has been removed, it's functionality is replaced by IResponse.
1616
- `RespondWith(this TestableHttpMessageHandler, HttpResponseMessage)` has been removed, the response is modified with every call, so it doesn't work reliably and is different from how HttpClientHandler works, which creates a HttpResponseMessage for every request.
1717
- `HttpResponseMessageBuilder` and `RespondWith(this TestableHttpMessageHandler, HttpResponseMessageBuilder)` has been removed, it's functionality can be replaced with ConfiguredResponse or a custom IResponse.
18+
- `HttpResponseContext` now has an internal constructor instead of a public one.
1819

1920
### Added
2021
- URI patterns now support query parameters and by default will use the unescaped values, note that the order is still important.
2122
- URI pattern parsing is extended to be able to parse most URI's.
23+
- `TestableHttpMessageHandler.ClearRequests` was added for situations where it is not possible to create and use a new instance.
2224

2325
### Changed
2426
- Use the same parser for the assertion methods `WithRequestUri` (which is used by `ShouldHaveMadeRequestsTo`) as for the RoutingResponse functionality.
2527
- `RouteParserException` has been renamed to `UriPatternParserException`.
2628
- Renamed `RoutingOptions` to `UriPatternMatchingOptions`.
29+
- `SequencedResponse` now is able to recover from a reset.
2730

2831
## [0.9] - 2022-11-25
2932
### Deprecated

src/TestableHttpClient/HttpResponseContext.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
/// </summary>
66
public class HttpResponseContext
77
{
8-
public HttpResponseContext(HttpRequestMessage httpRequestMessage, HttpResponseMessage httpResponseMessage, TestableHttpMessageHandlerOptions? options = null)
8+
internal HttpResponseContext(HttpRequestMessage httpRequestMessage, IReadOnlyCollection<HttpRequestMessage> httpRequestMessages, HttpResponseMessage httpResponseMessage, TestableHttpMessageHandlerOptions? options = null)
99
{
1010
HttpRequestMessage = httpRequestMessage;
11+
HttpRequestMessages = httpRequestMessages;
1112
HttpResponseMessage = httpResponseMessage;
1213
Options = options ?? new TestableHttpMessageHandlerOptions();
1314
}
@@ -17,8 +18,15 @@ public HttpResponseContext(HttpRequestMessage httpRequestMessage, HttpResponseMe
1718
/// </summary>
1819
public HttpRequestMessage HttpRequestMessage { get; }
1920
/// <summary>
21+
/// The requests that were send by the HttpClient.
22+
/// </summary>
23+
public IReadOnlyCollection<HttpRequestMessage> HttpRequestMessages { get; }
24+
/// <summary>
2025
/// The response message that will be send back to the HttpClient.
2126
/// </summary>
2227
public HttpResponseMessage HttpResponseMessage { get; }
28+
/// <summary>
29+
/// The options that can be used by different responses.
30+
/// </summary>
2331
public TestableHttpMessageHandlerOptions Options { get; }
2432
}

src/TestableHttpClient/PublicAPI.Shipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ static TestableHttpClient.Responses.Timeout() -> TestableHttpClient.IResponse!
4343

4444
TestableHttpClient.HttpResponseContext
4545
TestableHttpClient.HttpResponseContext.HttpRequestMessage.get -> System.Net.Http.HttpRequestMessage!
46-
TestableHttpClient.HttpResponseContext.HttpResponseContext(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpResponseMessage! httpResponseMessage, TestableHttpClient.TestableHttpMessageHandlerOptions? options = null) -> void
4746
TestableHttpClient.HttpResponseContext.HttpResponseMessage.get -> System.Net.Http.HttpResponseMessage!
4847
TestableHttpClient.HttpResponseContext.Options.get -> TestableHttpClient.TestableHttpMessageHandlerOptions!
4948

src/TestableHttpClient/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
TestableHttpClient.TestableHttpMessageHandlerOptions.UriPatternMatchingOptions.get -> TestableHttpClient.UriPatternMatchingOptions!
1+
TestableHttpClient.HttpResponseContext.HttpRequestMessages.get -> System.Collections.Generic.IReadOnlyCollection<System.Net.Http.HttpRequestMessage!>!
2+
TestableHttpClient.TestableHttpMessageHandler.ClearRequests() -> void
3+
TestableHttpClient.TestableHttpMessageHandlerOptions.UriPatternMatchingOptions.get -> TestableHttpClient.UriPatternMatchingOptions!
24
TestableHttpClient.UriPatternMatchingOptions
35
TestableHttpClient.UriPatternMatchingOptions.DefaultQueryFormat.get -> System.UriFormat
46
TestableHttpClient.UriPatternMatchingOptions.DefaultQueryFormat.set -> void
Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
1-
using System.Collections.Concurrent;
2-
3-
namespace TestableHttpClient.Response;
1+
namespace TestableHttpClient.Response;
42

53
internal class SequencedResponse : IResponse
64
{
7-
private readonly ConcurrentQueue<IResponse> responses;
8-
private readonly IResponse _lastResponse;
5+
private readonly List<IResponse> responses;
96
public SequencedResponse(IEnumerable<IResponse> responses)
107
{
118
this.responses = new(responses ?? throw new ArgumentNullException(nameof(responses)));
12-
if (this.responses.IsEmpty)
9+
if (this.responses.Count == 0)
1310
{
1411
throw new ArgumentException("Responses can't be empty.", nameof(responses));
1512
}
16-
_lastResponse = this.responses.Last();
1713
}
1814

1915
public Task ExecuteAsync(HttpResponseContext context, CancellationToken cancellationToken)
2016
{
21-
var response = GetResponse();
22-
return response.ExecuteAsync(context, cancellationToken);
23-
}
17+
int responseIndex = Math.Min(responses.Count - 1, context.HttpRequestMessages.Count - 1);
2418

25-
private IResponse GetResponse()
26-
{
27-
return responses.TryDequeue(out var response) ? response : _lastResponse;
19+
IResponse response = responses[responseIndex];
20+
return response.ExecuteAsync(context, cancellationToken);
2821
}
2922
}

src/TestableHttpClient/TestableHttpMessageHandler.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
using System.Collections.Concurrent;
2-
3-
using TestableHttpClient.Response;
1+
using TestableHttpClient.Response;
42

53
namespace TestableHttpClient;
64
/// <summary>
75
/// A testable HTTP message handler that captures all requests and always returns the same response.
86
/// </summary>
97
public class TestableHttpMessageHandler : HttpMessageHandler
108
{
11-
private readonly ConcurrentQueue<HttpRequestMessage> httpRequestMessages = new();
9+
private readonly Queue<HttpRequestMessage> httpRequestMessages = new();
10+
1211
private IResponse response = new HttpResponse(HttpStatusCode.OK);
1312

1413
public TestableHttpMessageHandlerOptions Options { get; } = new TestableHttpMessageHandlerOptions();
@@ -24,7 +23,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
2423

2524
HttpResponseMessage responseMessage;
2625
responseMessage = new();
27-
HttpResponseContext context = new(request, responseMessage, Options);
26+
HttpResponseContext context = new(request, httpRequestMessages, responseMessage, Options);
2827
await response.ExecuteAsync(context, cancellationToken).ConfigureAwait(false);
2928

3029
if (responseMessage.RequestMessage is null)
@@ -54,4 +53,15 @@ public void RespondWith(IResponse response)
5453
{
5554
this.response = response ?? throw new ArgumentNullException(nameof(response));
5655
}
56+
57+
/// <summary>
58+
/// Clear the registration of requests that were made with this handler.
59+
/// </summary>
60+
/// Sometimes the TestableHttpMessageHandler can't be replaced with a new instance, but it can be cleared.
61+
/// The configuration is not cleared and will be kept the same.
62+
/// <remarks>The configuration it self (Options and the configure IResponse) will not be cleared or reset.</remarks>
63+
public void ClearRequests()
64+
{
65+
httpRequestMessages.Clear();
66+
}
5767
}

test/TestableHttpClient.IntegrationTests/UsingIHttpClientFactory.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ public class UsingIHttpClientFactory
1515
public async Task ConfigureIHttpClientFactoryToUseTestableHttpClient()
1616
{
1717
// Create TestableHttpMessageHandler as usual.
18-
using var testableHttpMessageHandler = new TestableHttpMessageHandler();
18+
using TestableHttpMessageHandler testableHttpMessageHandler = new();
1919
testableHttpMessageHandler.RespondWith(StatusCode(HttpStatusCode.NoContent));
2020

21-
var services = new ServiceCollection();
21+
ServiceCollection services = new();
2222
// Register an HttpClient and configure the TestableHttpMessageHandler as the PrimaryHttpMessageHandler
2323
services.AddHttpClient(string.Empty).ConfigurePrimaryHttpMessageHandler(() => testableHttpMessageHandler);
2424

@@ -27,12 +27,18 @@ public async Task ConfigureIHttpClientFactoryToUseTestableHttpClient()
2727
// Request the IHttpClientFactory
2828
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
2929
// Create the HttpClient
30-
using var client = httpClientFactory.CreateClient();
30+
using HttpClient client = httpClientFactory.CreateClient();
3131
// And use it...
3232
_ = await client.GetAsync("https://httpbin.com/get");
3333

3434
// Now use the assertions to make sure the request was actually made.
3535
testableHttpMessageHandler.ShouldHaveMadeRequestsTo("https://httpbin.com/get");
36+
37+
// Since we already have created the serviceProvider, we can't (easily) replace the TestableHttpMessageHandler.
38+
// So in case you need to start fresh, i.e. when the serviceProvider is part of shared context, you can clear it.
39+
testableHttpMessageHandler.ClearRequests();
40+
41+
testableHttpMessageHandler.ShouldHaveMadeRequests(0);
3642
}
3743

3844
[Fact]

test/TestableHttpClient.Tests/Response/ConfiguredResponseTests.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.Threading;
2-
3-
using TestableHttpClient.Response;
1+
using TestableHttpClient.Response;
42

53
namespace TestableHttpClient.Tests.Response;
64

@@ -31,11 +29,9 @@ public async Task ExecuteAsync_ByDefault_CallsConfigureResponse()
3129
HttpResponse response = new(HttpStatusCode.OK);
3230

3331
void configureResponse(HttpResponseMessage _) => wasCalled = true;
34-
using HttpRequestMessage requestMessage = new();
35-
using HttpResponseMessage responseMessage = new();
3632
ConfiguredResponse sut = new(response, configureResponse);
3733

38-
await sut.ExecuteAsync(new HttpResponseContext(requestMessage, responseMessage), CancellationToken.None);
34+
using HttpResponseMessage responseMessage = await sut.TestAsync();
3935

4036
Assert.True(wasCalled, "configureResponse action was not called.");
4137
}

test/TestableHttpClient.Tests/Response/DelayedResponseTests.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
using System.Diagnostics;
2-
using System.Threading;
3-
4-
using TestableHttpClient.Response;
1+
using TestableHttpClient.Response;
52

63
namespace TestableHttpClient.Tests.Response;
74

@@ -22,12 +19,10 @@ public void Constructor_NullResponse_ThrowsArgumentNullException()
2219
[Fact]
2320
public async Task GetResponseAsync_ByDefault_ReturnsInnerResponse()
2421
{
25-
using HttpRequestMessage requestMessage = new();
26-
using HttpResponseMessage responseMessage = new();
2722
HttpResponse delayedResponse = new(HttpStatusCode.Created);
2823
DelayedResponse sut = new(delayedResponse, TimeSpan.Zero);
2924

30-
await sut.ExecuteAsync(new HttpResponseContext(requestMessage, responseMessage), CancellationToken.None);
25+
using HttpResponseMessage responseMessage = await sut.TestAsync();
3126

3227
Assert.Equal(HttpStatusCode.Created, responseMessage.StatusCode);
3328
}

test/TestableHttpClient.Tests/Response/HttpResponseTests.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@ public async Task ExecuteAsync_WithNullContext_ThrowsArgumentNullException()
1919
[Fact]
2020
public async Task ExecuteAsync_WithHttpStatusCode_ReturnsCorrectStatusCode()
2121
{
22-
using HttpRequestMessage requestMessage = new();
23-
using HttpResponseMessage responseMessage = new();
2422
HttpResponse sut = new(HttpStatusCode.Continue);
2523

26-
await sut.ExecuteAsync(new HttpResponseContext(requestMessage, responseMessage), CancellationToken.None);
24+
using HttpResponseMessage responseMessage = await sut.TestAsync();
2725

2826
Assert.Equal(HttpStatusCode.Continue, responseMessage.StatusCode);
2927
Assert.Equal(HttpStatusCode.Continue, sut.StatusCode);

0 commit comments

Comments
 (0)