Как выполнить модульное тестирование / зависимость, внедрить класс, зависящий от HttpClient с пользовательской конфигурацией HttpClientHandler - PullRequest
3 голосов
/ 28 апреля 2020

Я ищу предложения о том, как улучшить мой текущий дизайн для тестирования класса (пример ниже), который зависит от HttpClient с настраиваемой HttpClientHandler конфигурацией. Обычно я использую инъекцию в конструктор, чтобы внедрить HttpClient, который является единым для всего приложения, однако, поскольку он находится в библиотеке классов, я не могу полагаться на потребителей библиотеки для правильной настройки HttpClientHandler.

Для тестирования я следую стандартному подходу замены HttpClientHandler в конструкторе HttpClient. Поскольку я не могу полагаться на то, что потребитель библиотеки вставит действительный HttpClient, я не помещаю это в конструктор publi c, вместо этого я использую закрытый конструктор с внутренним методом stati c ( CreateWithCustomHttpClient()) чтобы создать его. Цель этого:

  • Закрытый конструктор не должен вызываться библиотекой внедрения зависимостей автоматически. Я знаю, что если я сделаю его общедоступным / внутренним, то некоторые библиотеки DI, для которых уже зарегистрирован HttpClient, вызовут этот конструктор.
  • Внутренний метод stati c может быть вызван библиотекой модульного тестирования с использованием 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();
    }
}

1 Ответ

1 голос
/ 28 апреля 2020

По моему мнению, я бы использовал IHttpClientFactory в Microsoft.Extensions.Http и создал бы пользовательское расширение для внедрения зависимостей для потребителей библиотеки классов использовать:

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 для тестирования цель.

...