Проблема в том, что у вас есть скрытая зависимость, а именно HttpClient
.Поскольку вы обновляете это в своих действиях, насмехаться невозможно.Вместо этого вы должны внедрить эту зависимость в свой контроллер.С ASP.NET Core 2.1+ это возможно с HttpClient
благодаря IHttpClientFactory
.Однако из коробки вы не можете внедрить HttpClient
непосредственно в контроллер, потому что контроллеры не зарегистрированы в коллекции сервисов.Несмотря на то, что вы можете изменить это, рекомендуется вместо этого создать класс «service».Это на самом деле лучше в любом случае, поскольку абстрагирует знания о взаимодействии с этим API из вашего контроллера полностью.Короче говоря, вы должны сделать что-то вроде:
public class ExternalApiService
{
private readonly HttpClient _httpClient;
public ExternalApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public Task<ExternalReponseType> GetExternalResponseAsync(int inputParam) =>
_httpClient.GetAsync($"/endpoint?param={inputParam}");
}
Затем зарегистрируйте это в ConfigureServices
:
services.AddHttpClient<ExternalApiService>(c =>
{
c.BaseAddress = new Uri("http://www.external.com");
});
И, наконец, введите его в свой контроллер:
public class MyController : ControllerBase
{
private readonly ExternalApiService _externalApi;
public MyController(ExternalApiService externalApi)
{
_externalApi = externalApi;
}
[HttpPost("MyAction")]
public async Task MyAction([FromBody] int inputParam)
{
var externalResponse = await _externalApi.GetExternalResponseAsync(inputParam);
//more work with the externalResponse
}
}
Теперь логика работы с этим API абстрагирована от вашего контроллера, и у вас есть зависимость, которую вы легко можете смоделировать.Так как вы хотите провести интеграционное тестирование, вам потребуется подключить другую реализацию сервиса при тестировании.Для этого я бы на самом деле сделал немного дальнейшей абстракции.Сначала создайте интерфейс для ExternalApiService
и заставьте сервис реализовать это.Затем в своем тестовом проекте вы можете создать альтернативную реализацию, которая полностью обходит HttpClient
и просто возвращает предварительно сделанные ответы.Затем, хотя это и не является строго необходимым, я бы создал расширение IServiceCollection
для абстрагирования вызова AddHttpClient
, что позволит вам повторно использовать эту логику, не повторяя себя:
public static class IServiceCollectionExtensions
{
public static IServiceCollection AddExternalApiService<TImplementation>(this IServiceCollection services, string baseAddress)
where TImplementation : class, IExternalApiService
{
services.AddHttpClient<IExternalApiService, TImplementation>(c =>
{
c.BaseAddress = new Uri(baseAddress)
});
return services;
}
}
, которую вы затем будете использовать следующим образом:
services.AddExternalApiService<ExternalApiService>("http://www.external.com");
Базовый адрес может (и, вероятно, должен) быть предоставлен через config, для дополнительного уровня абстракции / тестируемости.Наконец, вы должны использовать TestStartup
с WebApplicationFactory
.Это значительно упрощает переключение служб и других реализаций без переписывания всей вашей логики ConfigureServices
в Startup
, что, конечно, добавляет переменные в ваш тест: например, не работает ли это, потому что я забыл зарегистрировать что-то так же, как вмой настоящий Startup
?
Просто добавьте несколько виртуальных методов в ваш класс Startup
, а затем используйте их для таких вещей, как добавление баз данных и добавление службы здесь:
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
AddExternalApiService(services);
}
protected virtual void AddExternalApiService(IServiceCollection services)
{
services.AddExternalApiService<ExternalApiService>("http://www.external.com");
}
}
Затем в вашем тестовом проекте вы можете извлечь из Startup
и переопределить этот и подобные методы:
public class TestStartup : MyAppAPI.Startup
{
protected override void AddExternalApiService(IServiceCollection services)
{
// sub in your test `IExternalApiService` implementation
services.AddExternalApiService<TestExternalApiService>("http://www.external.com");
}
}
Наконец, при получении тестового клиента:
var client = _factory.WithWebHostBuilder(b => b.UseStartup<TestStartup>()).CreateClient();
Фактический WebApplicationFactory
по-прежнему использует MyAppAPI.Startup
, поскольку этот параметр общего типа соответствует точке входа приложения, а не тому, какой класс Startup
используется.