Я ищу предложения о том, как улучшить мой текущий дизайн для тестирования класса (пример ниже), который зависит от 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();
}
}