如何使用自定义 HttpClientHandler 配置单元 test/dependency 注入依赖于 HttpClient 的 class
How to unit test/dependency inject a class reliant on HttpClient with a custom HttpClientHandler configuration
我正在寻找 关于如何改进我当前设计的建议 以测试 class(下面的示例)取决于 HttpClient
自定义 HttpClientHandler
配置。我通常使用构造函数注入来注入在整个应用程序中保持一致的 HttpClient,但是因为这是在 class 库中,所以我不能依赖库的使用者正确设置 HttpClientHandler
。
为了测试,我遵循在 HttpClient
构造函数中替换 HttpClientHandler
的标准方法。因为我不能依赖库的使用者来注入有效的 HttpClient
我没有把它放在 public 构造函数中,而是使用带有内部静态方法的私有构造函数( CreateWithCustomHttpClient()
) 来创建它。这背后的意图是:
- 私有构造函数不应由依赖注入库自动调用。我知道,如果我做到了 public/internal,那么一些已经注册了
HttpClient
的 DI 库将调用该构造函数。
- 单元测试库可以使用
InternalsVisibleToAttribute
调用内部静态方法
这个设置对我来说似乎很复杂,我希望有人可以提出改进建议,但是我知道这可能是非常主观的,所以如果有任何既定的模式或设计规则可以遵循如果能听到他们的消息,我将非常感激。
我包含 DownloadSomethingAsync()
方法只是为了演示为什么 HttpClientHandler
需要非标准配置。重定向响应的默认设置是在不返回响应的情况下自动在内部重定向,我需要重定向响应,以便我可以将其包装在报告下载进度的 class 中(该功能与此问题无关).
public class DemoClass
{
private static readonly HttpClient defaultHttpClient = new HttpClient(
new HttpClientHandler
{
AllowAutoRedirect = false
});
private readonly ILogger<DemoClass> logger;
private readonly HttpClient httpClient;
public DemoClass(ILogger<DemoClass> logger) : this(logger, defaultHttpClient) { }
private DemoClass(ILogger<DemoClass> logger, HttpClient httpClient)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
[Obsolete("This is only provided for testing and should not be used in calling code")]
internal static DemoClass CreateWithCustomHttpClient(ILogger<DemoClass> logger, HttpClient httpClient)
=> new DemoClass(logger, httpClient);
public async Task<FileSystemInfo> DownloadSomethingAsync(CancellationToken ct = default)
{
// Build the request
logger.LogInformation("Sending request for download");
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/downloadredirect");
// Send the request
HttpResponseMessage response = await httpClient.SendAsync(request, ct);
// Analyse the result
switch (response.StatusCode)
{
case HttpStatusCode.Redirect:
break;
case HttpStatusCode.NoContent:
return null;
default: throw new InvalidOperationException();
}
// Get the redirect location
Uri redirect = response.Headers.Location;
if (redirect == null)
throw new InvalidOperationException("Redirect response did not contain a redirect URI");
// Create a class to handle the download with progress tracking
logger.LogDebug("Wrapping release download request");
IDownloadController controller = new HttpDownloadController(redirect);
// Begin the download
logger.LogDebug("Beginning release download");
return await controller.DownloadAsync();
}
}
在我的看来,我会使用IHttpClientFactory in Microsoft.Extensions.Http,并为class库的消费者创建一个自定义依赖注入扩展来使用:
public static class DemoClassServiceCollectionExtensions
{
public static IServiceCollection AddDemoClass(
this IServiceCollection services,
Func<HttpMessageHandler> configureHandler = null)
{
// Configure named HTTP client with primary message handler
var builder= services.AddHttpClient(nameof(DemoClass));
if (configureHandler == null)
{
builder = builder.ConfigurePrimaryHttpMessageHandler(
() => new HttpClientHandler
{
AllowAutoRedirect = false
});
}
else
{
builder = builder.ConfigurePrimaryHttpMessageHandler(configureHandler);
}
services.AddTransient<DemoClass>();
return services;
}
}
在 DemoClass
中,使用 IHttpClientFactory
创建命名的 HTTP 客户端:
class DemoClass
{
private readonly HttpClient _client;
public DemoClass(IHttpClientFactory httpClientFactory)
{
// This named client will have pre-configured message handler
_client = httpClientFactory.CreateClient(nameof(DemoClass));
}
public async Task DownloadSomethingAsync()
{
// omitted
}
}
您可以要求消费者必须调用 AddDemoClass
才能使用 DemoClass
:
var services = new ServiceCollection();
services.AddDemoClass();
通过这种方式,您可以隐藏 HTTP 客户端构造的细节。
同时,在测试中,您可以将 IHttpClientFactory
模拟为 return HttpClient
以进行测试。
我正在寻找 关于如何改进我当前设计的建议 以测试 class(下面的示例)取决于 HttpClient
自定义 HttpClientHandler
配置。我通常使用构造函数注入来注入在整个应用程序中保持一致的 HttpClient,但是因为这是在 class 库中,所以我不能依赖库的使用者正确设置 HttpClientHandler
。
为了测试,我遵循在 HttpClient
构造函数中替换 HttpClientHandler
的标准方法。因为我不能依赖库的使用者来注入有效的 HttpClient
我没有把它放在 public 构造函数中,而是使用带有内部静态方法的私有构造函数( CreateWithCustomHttpClient()
) 来创建它。这背后的意图是:
- 私有构造函数不应由依赖注入库自动调用。我知道,如果我做到了 public/internal,那么一些已经注册了
HttpClient
的 DI 库将调用该构造函数。 - 单元测试库可以使用
InternalsVisibleToAttribute
调用内部静态方法
这个设置对我来说似乎很复杂,我希望有人可以提出改进建议,但是我知道这可能是非常主观的,所以如果有任何既定的模式或设计规则可以遵循如果能听到他们的消息,我将非常感激。
我包含 DownloadSomethingAsync()
方法只是为了演示为什么 HttpClientHandler
需要非标准配置。重定向响应的默认设置是在不返回响应的情况下自动在内部重定向,我需要重定向响应,以便我可以将其包装在报告下载进度的 class 中(该功能与此问题无关).
public class DemoClass
{
private static readonly HttpClient defaultHttpClient = new HttpClient(
new HttpClientHandler
{
AllowAutoRedirect = false
});
private readonly ILogger<DemoClass> logger;
private readonly HttpClient httpClient;
public DemoClass(ILogger<DemoClass> logger) : this(logger, defaultHttpClient) { }
private DemoClass(ILogger<DemoClass> logger, HttpClient httpClient)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
[Obsolete("This is only provided for testing and should not be used in calling code")]
internal static DemoClass CreateWithCustomHttpClient(ILogger<DemoClass> logger, HttpClient httpClient)
=> new DemoClass(logger, httpClient);
public async Task<FileSystemInfo> DownloadSomethingAsync(CancellationToken ct = default)
{
// Build the request
logger.LogInformation("Sending request for download");
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/downloadredirect");
// Send the request
HttpResponseMessage response = await httpClient.SendAsync(request, ct);
// Analyse the result
switch (response.StatusCode)
{
case HttpStatusCode.Redirect:
break;
case HttpStatusCode.NoContent:
return null;
default: throw new InvalidOperationException();
}
// Get the redirect location
Uri redirect = response.Headers.Location;
if (redirect == null)
throw new InvalidOperationException("Redirect response did not contain a redirect URI");
// Create a class to handle the download with progress tracking
logger.LogDebug("Wrapping release download request");
IDownloadController controller = new HttpDownloadController(redirect);
// Begin the download
logger.LogDebug("Beginning release download");
return await controller.DownloadAsync();
}
}
在我的看来,我会使用IHttpClientFactory in Microsoft.Extensions.Http,并为class库的消费者创建一个自定义依赖注入扩展来使用:
public static class DemoClassServiceCollectionExtensions
{
public static IServiceCollection AddDemoClass(
this IServiceCollection services,
Func<HttpMessageHandler> configureHandler = null)
{
// Configure named HTTP client with primary message handler
var builder= services.AddHttpClient(nameof(DemoClass));
if (configureHandler == null)
{
builder = builder.ConfigurePrimaryHttpMessageHandler(
() => new HttpClientHandler
{
AllowAutoRedirect = false
});
}
else
{
builder = builder.ConfigurePrimaryHttpMessageHandler(configureHandler);
}
services.AddTransient<DemoClass>();
return services;
}
}
在 DemoClass
中,使用 IHttpClientFactory
创建命名的 HTTP 客户端:
class DemoClass
{
private readonly HttpClient _client;
public DemoClass(IHttpClientFactory httpClientFactory)
{
// This named client will have pre-configured message handler
_client = httpClientFactory.CreateClient(nameof(DemoClass));
}
public async Task DownloadSomethingAsync()
{
// omitted
}
}
您可以要求消费者必须调用 AddDemoClass
才能使用 DemoClass
:
var services = new ServiceCollection();
services.AddDemoClass();
通过这种方式,您可以隐藏 HTTP 客户端构造的细节。
同时,在测试中,您可以将 IHttpClientFactory
模拟为 return HttpClient
以进行测试。