Skip to content

Commit 5d9348c

Browse files
authored
Add oveload to CreateClient for adding extra handlers. (#164)
1 parent 4f38316 commit 5d9348c

File tree

14 files changed

+199
-20
lines changed

14 files changed

+199
-20
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to TestableHttpClient will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and
55
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.8] - Unplanned
8+
9+
### Added
10+
- `CreateClient` now accepts `DelegateHandlers` in order to chain Handlers. The InnerHandler property of each handler is set automatically and the `TestableHttpMessageHandler` is automatically set as the last handler. This is showcased with Polly in the integration tests.
11+
712
## [0.7] - 2022-09-22
813
### Changed
914
- In 0.6 the debug symbols were embedded in the dll, so the pipeline couldn't upload the symbol package. This is corrected in 0.7 where the symbol package is correct.
@@ -207,6 +212,7 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
207212
- Automatically build project when pushing changes to github and when creating a pull request
208213
- Automatically deploy to NuGet when creating a tag in github
209214

215+
[0.7]: https://github.com/dnperfors/TestableHttpClient/compare/v0.6...v0.7
210216
[0.6]: https://github.com/dnperfors/TestableHttpClient/compare/v0.5...v0.6
211217
[0.5]: https://github.com/dnperfors/TestableHttpClient/compare/v0.4...v0.5
212218
[0.4]: https://github.com/dnperfors/TestableHttpClient/compare/v0.3...v0.4

TestableHttpClient.sln

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 16
4-
VisualStudioVersion = 16.0.29709.97
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.3.32901.215
55
MinimumVisualStudioVersion = 15.0.26124.0
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4C8914F8-D732-462B-978E-3BB5DBE547D7}"
77
EndProject
@@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1717
CHANGELOG.md = CHANGELOG.md
1818
LICENSE = LICENSE
1919
README.md = README.md
20+
version.json = version.json
2021
EndProjectSection
2122
EndProject
2223
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableHttpClient.IntegrationTests", "test\TestableHttpClient.IntegrationTests\TestableHttpClient.IntegrationTests.csproj", "{37A6C1C0-1117-43DE-BD15-290BC8AD32BE}"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace TestableHttpClient;
2+
3+
[AttributeUsage(AttributeTargets.Method)]
4+
internal sealed class AssertionMethodAttribute : Attribute { }

src/TestableHttpClient/HttpRequestMessageAsserter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ private void Assert(int? expectedCount = null)
4343
/// <param name="requestFilter">The filter to filter requests with before asserting.</param>
4444
/// <param name="condition">The name of the conditon, used in the exception message.</param>
4545
/// <returns>The <seealso cref="IHttpRequestMessagesCheck"/> for further assertions.</returns>
46+
[AssertionMethod]
4647
public IHttpRequestMessagesCheck WithFilter(Func<HttpRequestMessage, bool> requestFilter, string condition) => WithFilter(requestFilter, null, condition);
4748

4849
/// <summary>
@@ -51,8 +52,10 @@ private void Assert(int? expectedCount = null)
5152
/// <param name="requestFilter">The filter to filter requests with before asserting.</param>
5253
/// <param name="condition">The name of the conditon, used in the exception message.</param>
5354
/// <returns>The <seealso cref="IHttpRequestMessagesCheck"/> for further assertions.</returns>
55+
[AssertionMethod]
5456
public IHttpRequestMessagesCheck WithFilter(Func<HttpRequestMessage, bool> requestFilter, int expectedNumberOfRequests, string condition) => WithFilter(requestFilter, (int?)expectedNumberOfRequests, condition);
5557

58+
[AssertionMethod]
5659
public IHttpRequestMessagesCheck WithFilter(Func<HttpRequestMessage, bool> requestFilter, int? expectedNumberOfRequests, string condition)
5760
{
5861
if (!string.IsNullOrEmpty(condition))

src/TestableHttpClient/TestableHttpMessageHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
3434
{
3535
cancelationSource.Cancel(false);
3636
}
37-
throw new TaskCanceledException(new OperationCanceledException().Message);
37+
return Task.FromCanceled<HttpResponseMessage>(cancellationToken);
3838
}
3939

4040
return Task.FromResult(response);

src/TestableHttpClient/TestableHttpMessageHandlerAssertionExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public static class TestableHttpMessageHandlerAssertionExtensions
99
/// <returns>An <see cref="IHttpRequestMessagesCheck"/> that can be used for additional assertions.</returns>
1010
/// <exception cref="ArgumentNullException">handler is `null`</exception>
1111
/// <exception cref="HttpRequestMessageAssertionException">When no requests are made</exception>
12+
[AssertionMethod]
1213
public static IHttpRequestMessagesCheck ShouldHaveMadeRequests(this TestableHttpMessageHandler handler)
1314
{
1415
if (handler == null)
@@ -27,6 +28,7 @@ public static IHttpRequestMessagesCheck ShouldHaveMadeRequests(this TestableHttp
2728
/// <returns>An <see cref="IHttpRequestMessagesCheck"/> that can be used for additional assertions.</returns>
2829
/// <exception cref="ArgumentNullException">handler is `null`</exception>
2930
/// <exception cref="HttpRequestMessageAssertionException">When no requests are made</exception>
31+
[AssertionMethod]
3032
public static IHttpRequestMessagesCheck ShouldHaveMadeRequests(this TestableHttpMessageHandler handler, int expectedNumberOfRequests)
3133
{
3234
if (handler == null)
@@ -45,6 +47,7 @@ public static IHttpRequestMessagesCheck ShouldHaveMadeRequests(this TestableHttp
4547
/// <returns>An <see cref="IHttpRequestMessagesCheck"/> that can be used for additional assertions.</returns>
4648
/// <exception cref="ArgumentNullException">handler is `null` or pattern is `null`</exception>
4749
/// <exception cref="HttpRequestMessageAssertionException">When no requests are made</exception>
50+
[AssertionMethod]
4851
public static IHttpRequestMessagesCheck ShouldHaveMadeRequestsTo(this TestableHttpMessageHandler handler, string pattern)
4952
{
5053
if (handler == null)
@@ -69,6 +72,7 @@ public static IHttpRequestMessagesCheck ShouldHaveMadeRequestsTo(this TestableHt
6972
/// <returns>An <see cref="IHttpRequestMessagesCheck"/> that can be used for additional assertions.</returns>
7073
/// <exception cref="ArgumentNullException">handler is `null` or pattern is `null`</exception>
7174
/// <exception cref="HttpRequestMessageAssertionException">When no requests are made</exception>
75+
[AssertionMethod]
7276
public static IHttpRequestMessagesCheck ShouldHaveMadeRequestsTo(this TestableHttpMessageHandler handler, string pattern, int expectedNumberOfRequests)
7377
{
7478
if (handler == null)

src/TestableHttpClient/TestableHttpMessageHandlerExtensions.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ public static class TestableHttpMessageHandlerExtensions
99
/// <returns>An HttpClient configure with the TestableHttpMessageHandler.</returns>
1010
/// <exception cref="ArgumentNullException">The `handler` is `null`</exception>
1111
/// <remarks>Using this method is equivalent to `new HttClient(handler)`.</remarks>
12-
public static HttpClient CreateClient(this TestableHttpMessageHandler handler)
12+
public static HttpClient CreateClient(this TestableHttpMessageHandler handler, params DelegatingHandler[] httpMessageHandlers)
1313
{
14-
return CreateClient(handler, _ => { });
14+
return CreateClient(handler, _ => { }, httpMessageHandlers);
1515
}
1616

1717
/// <summary>
@@ -22,6 +22,11 @@ public static HttpClient CreateClient(this TestableHttpMessageHandler handler)
2222
/// <returns>An HttpClient configure with the TestableHttpMessageHandler.</returns>
2323
/// <exception cref="ArgumentNullException">The `handler` or `configureClient` is `null`</exception>
2424
public static HttpClient CreateClient(this TestableHttpMessageHandler handler, Action<HttpClient> configureClient)
25+
{
26+
return CreateClient(handler, configureClient, Enumerable.Empty<DelegatingHandler>());
27+
}
28+
29+
public static HttpClient CreateClient(this TestableHttpMessageHandler handler, Action<HttpClient> configureClient, IEnumerable<DelegatingHandler> httpMessageHandlers)
2530
{
2631
if (handler is null)
2732
{
@@ -33,9 +38,31 @@ public static HttpClient CreateClient(this TestableHttpMessageHandler handler, A
3338
throw new ArgumentNullException(nameof(configureClient));
3439
}
3540

36-
var httpClient = new HttpClient(handler);
41+
if (httpMessageHandlers is null)
42+
{
43+
throw new ArgumentNullException(nameof(httpMessageHandlers));
44+
}
45+
46+
if (httpMessageHandlers.Any(x => x is null))
47+
{
48+
throw new ArgumentNullException(nameof(httpMessageHandlers));
49+
}
50+
51+
var httpClient = new HttpClient(CreateHandlerChain(handler, httpMessageHandlers));
3752
configureClient(httpClient);
3853

3954
return httpClient;
4055
}
56+
57+
private static HttpMessageHandler CreateHandlerChain(TestableHttpMessageHandler handler, IEnumerable<DelegatingHandler> additionalHandlers)
58+
{
59+
HttpMessageHandler next = handler;
60+
var reversedHandlers = additionalHandlers.Reverse();
61+
foreach (var delegatingHandler in reversedHandlers)
62+
{
63+
delegatingHandler.InnerHandler = next;
64+
next = delegatingHandler;
65+
}
66+
return next;
67+
}
4168
}

test/TestableHttpClient.IntegrationTests/TestableHttpClient.IntegrationTests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
88
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="[3.1.*,5.0.0)" />
99
<PackageReference Include="Microsoft.Extensions.Http" Version="[3.1.*,5.0.0)" />
10+
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="[3.1.*,5.0.0)" />
1011
</ItemGroup>
1112

1213
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
1314
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="[6.0.*,)" />
1415
<PackageReference Include="Microsoft.Extensions.Http" Version="[6.0.*,)" />
16+
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="[6.0.*,)" />
1517
</ItemGroup>
1618

1719
<ItemGroup>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Microsoft.Extensions.Http;
2+
3+
using Polly;
4+
using Polly.Extensions.Http;
5+
6+
namespace TestableHttpClient.IntegrationTests;
7+
8+
public class TestingRetryMechanisms
9+
{
10+
[Fact]
11+
public async Task TestingRetryPolicies()
12+
{
13+
// Create TestableHttpMessageHandler as usual.
14+
using var testableHttpMessageHandler = new TestableHttpMessageHandler();
15+
testableHttpMessageHandler.RespondWith(response => response.WithHttpStatusCode(HttpStatusCode.ServiceUnavailable));
16+
17+
// Configure the retry policy
18+
var policy = HttpPolicyExtensions.HandleTransientHttpError().RetryAsync(2);
19+
using PolicyHttpMessageHandler retryPolicyHandler = new(policy);
20+
21+
using HttpClient client = testableHttpMessageHandler.CreateClient(retryPolicyHandler);
22+
23+
// Make a request, which should fail
24+
_ = await client.GetAsync("https://httpbin.com/get");
25+
26+
// Now use the assertions to make sure the request was actually made multiple times.
27+
testableHttpMessageHandler.ShouldHaveMadeRequestsTo("https://httpbin.com/get", 3);
28+
}
29+
30+
[Fact]
31+
public async Task SimulateTimeoutDoesNotRetry()
32+
{
33+
// Create TestableHttpMessageHandler as usual.
34+
using var testableHttpMessageHandler = new TestableHttpMessageHandler();
35+
testableHttpMessageHandler.SimulateTimeout();
36+
37+
// Configure the retry policy
38+
var policy = HttpPolicyExtensions.HandleTransientHttpError().RetryAsync(2);
39+
using PolicyHttpMessageHandler retryPolicyHandler = new(policy);
40+
41+
using HttpClient client = testableHttpMessageHandler.CreateClient(retryPolicyHandler);
42+
43+
try
44+
{
45+
_ = await client.GetAsync("https://httpbin.com/get");
46+
Assert.Fail("This should never be reached, since a timeout should throw an exception.");
47+
}
48+
catch (TaskCanceledException)
49+
{
50+
// Catch the TaskCanceledException, since we know we that is thrown when a timeout occurs
51+
}
52+
53+
// Now use the assertions to make sure the request was actually made once, so polly didn't run.
54+
testableHttpMessageHandler.ShouldHaveMadeRequestsTo("https://httpbin.com/get", 1);
55+
}
56+
}

test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClient.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ namespace TestableHttpClient.Tests;
44

55
public partial class TestableHttpMessageHandlerExtensionsTests
66
{
7-
#nullable disable
87
[Fact]
98
public void CreateClient_NullTestableHttpMessageHandler_ThrowsArgumentNullException()
109
{
11-
TestableHttpMessageHandler sut = null;
10+
TestableHttpMessageHandler sut = null!;
1211

1312
var exception = Assert.Throws<ArgumentNullException>(() => sut.CreateClient());
1413
Assert.Equal("handler", exception.ParamName);
1514
}
16-
#nullable restore
1715

1816
[Fact]
1917
public void CreateClient_CorrectTestableHttpMessageHandler_AddsHandlerToHttpClient()
@@ -27,14 +25,23 @@ public void CreateClient_CorrectTestableHttpMessageHandler_AddsHandlerToHttpClie
2725
Assert.Same(sut, handler);
2826
}
2927

30-
private static object? GetPrivateHandler(HttpClient client)
28+
[Fact]
29+
public void CreateClient_NullDelegateHandler_ThrowsArgumentNullException()
30+
{
31+
using TestableHttpMessageHandler sut = new();
32+
DelegatingHandler handler = null!;
33+
var exception = Assert.Throws<ArgumentNullException>(() => sut.CreateClient(handler));
34+
Assert.Equal("httpMessageHandlers", exception.ParamName);
35+
}
36+
37+
private static HttpMessageHandler? GetPrivateHandler(HttpClient client)
3138
{
3239
var handlerField = client.GetType().BaseType?.GetField("_handler", BindingFlags.Instance | BindingFlags.NonPublic);
3340
if (handlerField == null)
3441
{
3542
Assert.True(false, "Can't find the private _handler field on HttpClient.");
3643
return null;
3744
}
38-
return handlerField.GetValue(client);
45+
return handlerField.GetValue(client) as HttpMessageHandler;
3946
}
4047
}

0 commit comments

Comments
 (0)