【问题标题】:How to unit test/dependency inject a class reliant on HttpClient with a custom HttpClientHandler configuration如何使用自定义 HttpClientHandler 配置对依赖于 HttpClient 的类进行单元测试/依赖注入
【发布时间】:2020-04-28 09:41:01
【问题描述】:

我正在寻找关于如何改进我当前设计的建议,以使用自定义 HttpClientHandler 配置测试依赖于 HttpClient 的类(以下示例)。我通常使用构造函数注入来注入在整个应用程序中一致的 HttpClient,但是因为这是在类库中,所以我不能依赖库的使用者来正确设置 HttpClientHandler

为了进行测试,我遵循在HttpClient 构造函数中替换HttpClientHandler 的标准方法。因为我不能依赖库的使用者来注入有效的HttpClient,所以我没有将它放在公共构造函数中,而是使用带有内部静态方法(CreateWithCustomHttpClient())的私有构造函数来创建它。这背后的意图是:

  • 依赖注入库不应自动调用私有构造函数。我知道,如果我将其设为公共/内部,那么一些已注册 HttpClient 的 DI 库将调用该构造函数。
  • 单元测试库可以使用InternalsVisibleToAttribute调用内部静态方法

这个设置对我来说似乎很复杂,我希望有人可以提出改进建议,但我知道这可能是相当主观的,所以如果在这种情况下有任何既定的模式或设计规则要遵循,我非常感谢听到他们的消息。

我包含DownloadSomethingAsync() 方法只是为了演示为什么HttpClientHandler 需要非标准配置。默认是重定向响应在内部自动重定向而不返回响应,我需要重定向响应,以便我可以将它包装在一个报告下载进度的类中(该功能与此问题无关)。

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();
    }
}

【问题讨论】:

    标签: c# unit-testing dependency-injection inversion-of-control


    【解决方案1】:

    我的看来,我会在Microsoft.Extensions.Http 中使用IHttpClientFactory,并创建一个自定义依赖注入扩展供类库的消费者使用:

    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 以返回 HttpClient 以进行测试。

    【讨论】:

    • 好方法。您可以稍微更改它以使用类型化的客户端,而不是像您展示的那样手动将其添加为瞬态。它会为您完成所有样板设置。
    • 我非常喜欢这种方法。不幸的是,IHttpClientFactory 确实与 Extension.DependencyInjection (github.com/dotnet/extensions/issues/1345) 紧密耦合。我正在尝试开发该库,以便我们所有使用 Extensions.DependencyInjection 和 Autofac 的内部团队都可以轻松使用该库,并为根本不使用 DI 的项目提供良好的默认实现。这非常符合我的需求,我没有在问题中详细说明,所以就我而言,这个答案仍然有效。
    • @Brad 在这种情况下,您可能仍然会借用这个想法。就像提供一个工厂来实例化DemoService
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多