Как издеваться над httpMessageHandler с задержкой? - PullRequest
0 голосов
/ 26 марта 2020

Я создаю REST API, который будет вызывать (получать) n других REST API, объединять их ответы и затем отправлять их клиенту.

Я очень обеспокоен тем, что мой асинхронный метод, который вызывает другие услуги не так. У меня уже был тест, использующий moq, чтобы высмеивать ответы API. В течение нескольких дней, пока я тестировал приложение и выполнял тесты, оно работало, а затем на сервере сборки оно ломалось (в следующей попытке оно не ломалось). Очевидно, я знал, что это проблема с моими асинхронными вызовами.

Моя цель - создать надежный метод тестирования, имитирующий поведение и задержку многих вызовов API.

Рабочий код:

private async Task<List<T>> GetInformationFromExternalSource<T>(List<string> urls) where T : BaseDTO
        {
            var response = new List<T>();
            var tasks = new List<Task<List<T>>>();

            urls.ForEach(url =>
            {
                tasks.Add(Task.Run(() =>
                {
                    string infoJson = RequestDataFromProvider(url).Result;
                    List<T> result = JsonConvert.DeserializeObject<List<T>>(infoJson);
                    return result;
                }));
            });

            await Task.WhenAll(tasks.ToArray());
            tasks.ForEach(t => response.AddRange(t.Result));
            return response;
        }

        private virtual Task<string> RequestDataFromProvider(string url)
        {
            HttpClient client = _clientProvider.getClient();
            var result = client.GetAsync(url).Result;
            return result.Content.ReadAsStringAsync();
        }

Тестовый код:

        [Theory]
        [InlineData(3, 2, 5, 7, 17)]
        [InlineData(30, 20, 50, 70, 170)]
        [InlineData(1, 0, 0, 0, 1)]
        [InlineData(43, 1, 0, 0, 44)]
        [InlineData(1, 1, 1, 1, 4)]
        [InlineData(300, 100, 150, 50, 600)]
        [InlineData(0, 0, 0, 0, 0)]
        public async Task GetJsons_WhenHasResponseInManyCalls_ShouldConcatenate(int s1, int s2, int s3, int s4, int expCount)
        {
            //Arrange
            var fakeHttpMessageHandler = new Mock<FakeHttpMessageHandler>(); //here I also tried to use 
            //a SleepyHttpMessageHandler, but I threw a lot of aggregate exceptions because of my Thread.Sleep
            var client = new HttpClient(fakeHttpMessageHandler.Object);
            var providerMock = new Mock<IHttpProvider>();
            providerMock.Setup(f => f.getClient()).Returns(client);
            var myService = new MyService(providerMock.Object);

            //Arrange
            _fakeHttpMessageHandler.SetupSequence(f => f.Send(It.IsAny<HttpRequestMessage>()))
                .Returns(httpResponseWithContent(createDummyDto(s1)))
                .Returns(httpResponseWithContent(createDummyDto(s2)))
                .Returns(httpResponseWithContent(createDummyDto(s3)))
                .Returns(httpResponseWithContent(createDummyDto(s4))); //I mocked the DB to have only 4 urls
            //Act
            List<DummyDto> result = await myService.getRequest<DummyDto>(); //getAction will get the urls in database and then return GetInformationFromExternalSource<DummyDto>(urls)
            //Assert
            result.Should().HaveCount(expCount);
        }

        private static HttpResponseMessage httpResponseWithContent(List<DummyDto> content1)
        {
            return new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent(JsonConvert.SerializeObject(content1))
            };
        }

FakeHttpMessageHandler:

    public class FakeHttpMessageHandler : HttpMessageHandler
    {
        public virtual HttpResponseMessage Send(HttpRequestMessage request)
        {
            throw new NotImplementedException("Now we can setup this method with our mocking framework");
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            //Thread.Sleep(100); 
            return Task.FromResult(Send(request));
        }
    }

Я хочу подождать случайное время, пока ответ не будет отправлен потокам.

Я пробовал setupSequence, callBack, но все они привели к большему количеству неудачных тестов, чем к прохождению.

1 Ответ

0 голосов
/ 27 марта 2020

Не фактический ответ, но слишком большой для комментария.
Я бы предложил немного изменить производственный код, чтобы получить больше преимуществ от асинхронных вызовов.

private async Task<List<T>> GetInformationFromExternalSource<T>(List<string> urls) where T : BaseDTO
{
    var responseTasks = urls.Select(url => RequestDataFromProvider(url)).ToArray();

    await Task.WhenAll(responseTasks);

    // all tasks are complete at this moment, so we can safely access a .Result 
    return responseTasks
        .Select(task => task.Result)
        .SelectMany(json => JsonConvert.DeserializeObject<List<T>>(json));
}

private virtual async Task<string> RequestDataFromProvider(string url)
{
    var client = _clientProvider.getClient();
    var result = await client.GetAsync(url);

    return await result.Content.ReadAsStringAsync();
}

Что меня беспокоило в исходном коде, так это то, что вы оборачивали уже асинхронные методы другой задачей.
Асинхронный подход позволяет нам получать доступ к внешним ресурсам без блокировки без дополнительных потоков (Task.Run будет создать новый поток)

Обратите внимание, что код urls.Select(url => RequestDataFromProvider(url)).ToArray(); отправит следующий http-запрос, не дожидаясь завершения предыдущего запроса, все запросы будут отправлены почти одновременно.

В некоторых API есть ограничение на количество входящих запросов в минуту или в секунду, если такое ограничение существует, вы должны проверить с API, который вы используете.

Вы не сказали, что подразумеваете под «оно сломалось» , поэтому найти реальную проблему, не зная, какое поведение вы наблюдаете, невозможно.

О фальсификации HttpMessageHandler - вы уже создали фальшивую реализацию, поэтому вам больше не нужен Mock для этого, не стесняйтесь создавать фальшивые, которые удовлетворят ваши потребности

public class FakeHttpMessageHandler : HttpMessageHandler
{
    private readonly Queue<(HttpResponseMessage Message, int Delay)> _queue;

    public FakeHttpMessageHandler()
    { 
        _queue = new Queue<(HttpResponseMessage Message, int Delay)>();
    }

    public void Add(HttpResponseMessage response, int delay)
    {
        _queue.Enqueue((response, delay));
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var (message, delay) = _queue.Dequeue();

        await Task.Delay(delay, cancellationToken);

        return message;
    }
}

Использование по вашему усмотрению поддельный обработчик

[Fact]
public async Task ReturnGivenResponse()
{
    var fakeHandler = new FakeHttpMessageHandler();
    fakeHandler.Add(new HttpResponseMessage(HttpStatusCode.OK), 1000);
    fakeHandler.Add(new HttpResponseMessage(HttpStatusCode.NotFound), 1000);

    var client = new HttpClient(fakeHandler);
    var request = new HttpRequestMessage(HttpMethod.Get, "https://www.fake.com/api/fake");

    var response = await client.SendAsync(request);

    response.StatusCode.Should().Be(HttpStatusCode.Accepted);
}

Ответ на оригинальный вопрос о том, как макетировать обработчик, чтобы иметь задержку.
Moq предоставляет возможность симулировать защищенные методы.

[Fact]
public async Task WithMoq()
{
    var mock = new Mock<HttpMessageHandler>();
    mock.Protected().SetupSequence<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
        .Returns(async () =>
        {
            await Task.Delay(1000);
            return new HttpResponseMessage(HttpStatusCode.OK);
        })
        .Returns(async () =>
        {
            await Task.Delay(1000);
            return new HttpResponseMessage(HttpStatusCode.NotFound);
        });

    var client = new HttpClient(mock.Object);
    var request = new HttpRequestMessage(HttpMethod.Get, "https://www.fake.com/api/fake");

    var response = await client.SendAsync(request);

    response.StatusCode.Should().Be(HttpStatusCode.OK);
}
...