Skip to content

Commit a7979de

Browse files
authored
Refactor response configuration (#168)
Configuring responses now has a complete overhaul. By using IResponse, it is now possible to create delayed and sequenced responses and in general it is more flexible. All old response configuration options are deprecated as a result, so tests that use these configurations have to be refactored.
1 parent 145016a commit a7979de

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1431
-170
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [0.8] - Unplanned
8+
### Deprecated
9+
- `TestableHttpMessageHandler.SimulateTimeout` is deprecated, and can be replaced with `RespondWith(Responses.Timeout())`.
10+
- `TestableHttpMessageHandler.RespondWith(Func<HttpRequestMessage, HttpResponseMessage>)` had been deprecated, it's functionality is replaced by IResponse.
11+
- `RespondWith(this TestableHttpMessageHandler, HttpResponseMessage)` has been deprecated, 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.
12+
- `HttpResponseMessageBuilder` and `RespondWith(this TestableHttpMessageHandler, HttpResponseMessageBuilder)` has been deprecated, it's functionality can be replaced with ConfiguredResponse or a custom IResponse.
813

914
### Added
1015
- `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.
1116
- Added support for .NET Framework 4.6.2, .NET Framework 4.7 and .NET Framework 4.8 by running the tests against these versions.
17+
- Added several `Responses`, including `Delayed`, `Timeout`, `Configured`, `Sequenced`, `StatusCode` and `Json`. These responses can now be used inside the `RespondWith`.
18+
19+
### Changed
20+
- `TestableHttpClient` now works with the `Responses` class, making it easier to configure responses.
21+
- When `HttpResponseMessage.Content` is null after `IResponse.ExecuteAsync` was called, an empty `StringContent` is added (Up until .NET 6.0, since Content is always filled there).
22+
- The `HttpRequestMessage` is always added to the response, which is now possible, since we no longer allow reusing responses.
23+
- Added `ConfigureAwait(false)` to all calls, since we now use async/await in the library.
1224

1325
## [0.7] - 2022-09-22
1426
### Changed

global.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"sdk": {
3+
"version": "6.0.203",
4+
"allowPrerelease": false,
5+
"rollForward": "latestMajor"
6+
}
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// This file is used to register types that are normally generated by the compiler
2+
// but aren't generated for older versions of .NET
3+
#if !NET6_0_OR_GREATER
4+
using System.ComponentModel;
5+
6+
namespace System.Runtime.CompilerServices;
7+
8+
// This class is used for init setters, which are introduced in .NET 6
9+
[EditorBrowsable(EditorBrowsableState.Never)]
10+
internal static class IsExternalInit { }
11+
#endif
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace TestableHttpClient;
2+
3+
/// <summary>
4+
/// This class contains contextual information for generating responses.
5+
/// </summary>
6+
public class HttpResponseContext
7+
{
8+
public HttpResponseContext(HttpRequestMessage httpRequestMessage, HttpResponseMessage httpResponseMessage)
9+
{
10+
HttpRequestMessage = httpRequestMessage;
11+
HttpResponseMessage = httpResponseMessage;
12+
}
13+
14+
/// <summary>
15+
/// The request message that is send by the HttpClient.
16+
/// </summary>
17+
public HttpRequestMessage HttpRequestMessage { get; }
18+
/// <summary>
19+
/// The response message that will be send back to the HttpClient.
20+
/// </summary>
21+
public HttpResponseMessage HttpResponseMessage { get; }
22+
}

src/TestableHttpClient/HttpResponseMessageBuilder.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@ namespace TestableHttpClient;
66
/// This class helps creating an <see cref="HttpResponseMessage"/> using a fluent interface.
77
/// </summary>
88
[SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "The HttpResponseMessage is only created and passed to the consumer.")]
9+
[Obsolete("Use ConfiguredResponse or a custom IResponse instead.")]
910
public sealed class HttpResponseMessageBuilder
1011
{
11-
private readonly HttpResponseMessage httpResponseMessage = new HttpResponseMessage
12+
private readonly HttpResponseMessage httpResponseMessage;
13+
14+
public HttpResponseMessageBuilder()
15+
{
16+
httpResponseMessage = new HttpResponseMessage
17+
{
18+
Content = new StringContent("")
19+
};
20+
}
21+
22+
internal HttpResponseMessageBuilder(HttpResponseMessage httpResponseMessage)
1223
{
13-
Content = new StringContent("")
14-
};
24+
this.httpResponseMessage = httpResponseMessage ?? throw new ArgumentNullException(nameof(httpResponseMessage));
25+
}
1526

1627
/// <summary>
1728
/// Specifies the version of the response.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace TestableHttpClient;
2+
3+
/// <summary>
4+
/// This interface describes how responses should be implemented.
5+
/// </summary>
6+
public interface IResponse
7+
{
8+
/// <summary>
9+
/// Execute the response and fill the HttpResponseMessage on the HttpResponseContext.
10+
/// </summary>
11+
/// <param name="context">The context for this request.</param>
12+
/// <param name="cancellationToken">The cancellationToken.</param>
13+
/// <returns></returns>
14+
Task ExecuteAsync(HttpResponseContext context, CancellationToken cancellationToken);
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace TestableHttpClient;
4+
5+
/// <summary>
6+
/// Provides an interface for registering external methods that provide
7+
/// custom <see cref="IResponse" /> instances.
8+
/// </summary>
9+
[SuppressMessage("Design", "CA1040:Avoid empty interfaces", Justification = "Used for extending Responses class")]
10+
public interface IResponsesExtensions { }
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace TestableHttpClient.Response;
2+
3+
[Obsolete("Use ConfiguredResponse or a custom IResponse instead.")]
4+
internal class BuilderResponse : IResponse
5+
{
6+
private readonly Action<HttpResponseMessageBuilder> httpResponseMessageBuilderAction;
7+
8+
internal BuilderResponse(Action<HttpResponseMessageBuilder> httpResponseMessageBuilderAction)
9+
{
10+
this.httpResponseMessageBuilderAction = httpResponseMessageBuilderAction ?? throw new ArgumentNullException(nameof(httpResponseMessageBuilderAction));
11+
}
12+
13+
public Task ExecuteAsync(HttpResponseContext context, CancellationToken cancellationToken)
14+
{
15+
HttpResponseMessageBuilder builder = new(context.HttpResponseMessage);
16+
httpResponseMessageBuilderAction(builder);
17+
18+
return Task.CompletedTask;
19+
}
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace TestableHttpClient.Response;
2+
3+
internal class ConfiguredResponse : IResponse
4+
{
5+
private readonly IResponse innerResponse;
6+
private readonly Action<HttpResponseMessage> configureResponse;
7+
8+
public ConfiguredResponse(IResponse response, Action<HttpResponseMessage> configureResponse)
9+
{
10+
innerResponse = response ?? throw new ArgumentNullException(nameof(response));
11+
this.configureResponse = configureResponse ?? throw new ArgumentNullException(nameof(configureResponse));
12+
}
13+
14+
public async Task ExecuteAsync(HttpResponseContext context, CancellationToken cancellationToken)
15+
{
16+
await innerResponse.ExecuteAsync(context, cancellationToken).ConfigureAwait(false);
17+
configureResponse(context.HttpResponseMessage);
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace TestableHttpClient.Response;
2+
3+
internal class DelayedResponse : IResponse
4+
{
5+
private readonly IResponse delayedResponse;
6+
private readonly TimeSpan delay;
7+
8+
public DelayedResponse(IResponse delayedResponse, TimeSpan delay)
9+
{
10+
this.delayedResponse = delayedResponse ?? throw new ArgumentNullException(nameof(delayedResponse));
11+
this.delay = delay;
12+
}
13+
14+
public async Task ExecuteAsync(HttpResponseContext context, CancellationToken cancellationToken)
15+
{
16+
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
17+
await delayedResponse.ExecuteAsync(context, cancellationToken).ConfigureAwait(false);
18+
}
19+
}

0 commit comments

Comments
 (0)