Тестовое действие контроллера, которое выполняет внешний вызов API - PullRequest
0 голосов
/ 18 февраля 2019

У меня есть контроллер API с функцией действия.Эта функция выполняет внешний вызов другого API для получения некоторых данных.этот внешний вызов делается простым созданием клиента с URL.Я хочу создать тест с использованием WebApplicationFactory для тестирования этой функции действия.Я хотел бы знать, как настроить этот внешний вызов.Чтобы сказать, если сервер вызывает этот URL, верните этот ответ.

Может быть, это должно быть где-то в переопределении ConfigureWebHost, чтобы сообщить серверу, что если вы вызываете этот URL (URL внешнего API), верните этот ответ.

Вот действие контроллера, которое я хочу проверить.

namespace MyAppAPI.Controllers
{
    public class MyController : ControllerBase
    {
        [HttpPost("MyAction")]
        public async Task MyAction([FromBody] int inputParam)
        {
            var externalApiURL = "http://www.external.com?param=inputParam";
            var client = new HttpClient();
            var externalResponse = await client.GetAsync(externalApiURL);
            //more work with the externalResponse
        }
    }
}

Вот класс теста, который я хочу использовать

public class MyAppAPITests : IClassFixture<WebApplicationFactory<MyAppAPI.Startup>>
{
     private readonly WebApplicationFactory<MyAppAPI.Startup> _factory;

     public MyAppAPITests(WebApplicationFactory<MyAppAPI.Startup> factory)
     {
          _factory = factory;
     }

     [Fact]
     public async Task Test_MyActionReturnsExpectedResponse()
     {
          //Arrange Code

          //Act
          //Here I would like to have something like this or a similar fashion
          _factory.ConfigureReponseForURL("http://www.external.com?param=inputParam",
                   response => {
                         response.Response = "ExpectedResponse";
                   });

          //Assert Code
     }
}

Код в Test_MyActionReturnsExpectedResponse нигде не существует, онэто именно то, что я хотел бы получить либо путем наследования WebApplicationFactory или путем его настройки.Я хотел бы знать, как этого можно достичь.т.е. настройка ответа, когда контроллер API выполняет внешний вызов.Спасибо за вашу помощь.

Ответы [ 2 ]

0 голосов
/ 18 февраля 2019

Проблема в том, что у вас есть скрытая зависимость, а именно 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 используется.

0 голосов
/ 18 февраля 2019

Я думаю, что лучший способ - я использовать интерфейсы и MOCK.Реализуйте интерфейс наследованием HttpClient и при тестировании смоделируйте этот интерфейс:

    public interface IHttpClientMockable
    {
        Task<string> GetStringAsync(string requestUri);
        Task<string> GetStringAsync(Uri requestUri);
        Task<byte[]> GetByteArrayAsync(string requestUri);
        Task<byte[]> GetByteArrayAsync(Uri requestUri);
        Task<Stream> GetStreamAsync(string requestUri);
        Task<Stream> GetStreamAsync(Uri requestUri);
        Task<HttpResponseMessage> GetAsync(string requestUri);
        Task<HttpResponseMessage> GetAsync(Uri requestUri);
        Task<HttpResponseMessage> GetAsync(string requestUri, HttpCompletionOption completionOption);
        Task<HttpResponseMessage> GetAsync(Uri requestUri, HttpCompletionOption completionOption);
        Task<HttpResponseMessage> GetAsync(string requestUri, CancellationToken cancellationToken);
        Task<HttpResponseMessage> GetAsync(Uri requestUri, CancellationToken cancellationToken);
        Task<HttpResponseMessage> GetAsync(string requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
        Task<HttpResponseMessage> GetAsync(Uri requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
        Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
        Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content);
        Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content, CancellationToken cancellationToken);
        Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
        Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content);
        Task<HttpResponseMessage> PutAsync(Uri requestUri, HttpContent content);
        Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content, CancellationToken cancellationToken);
        Task<HttpResponseMessage> PutAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken);
        Task<HttpResponseMessage> DeleteAsync(string requestUri);
        Task<HttpResponseMessage> DeleteAsync(Uri requestUri);
        Task<HttpResponseMessage> DeleteAsync(string requestUri, CancellationToken cancellationToken);
        Task<HttpResponseMessage> DeleteAsync(Uri requestUri, CancellationToken cancellationToken);
        Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
        Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
        Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption);
        Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken);
        void CancelPendingRequests();
        HttpRequestHeaders DefaultRequestHeaders { get; }
        Uri BaseAddress { get; set; }
        TimeSpan Timeout { get; set; }
        long MaxResponseContentBufferSize { get; set; }
        void Dispose();
    }

    public class HttpClientMockable: HttpClient, IHttpClientMockable
    {

    }
...