FakeItEasy - можно ли асинхронно тестировать ограничения (т.е. MatchesAsync)? - PullRequest
3 голосов
/ 05 марта 2019

Я столкнулся с трудностями при тестировании System.Net.Http.HttpClient с помощью FakeItEasy.Рассмотрим следующий сценарий:

//Service that consumes HttpClient
public class LoggingService
{
    private readonly HttpClient _client;

    public LoggingService(HttpClient client)
    {
        _client = client;
        _client.BaseAddress = new Uri("http://www.example.com");
    }

    public async Task Log(LogEntry logEntry)
    {
        var json = JsonConvert.SerializeObject(logEntry);
        var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
        await _client.PostAsync("/api/logging", httpContent);
    }
}

public class LogEntry
{
    public string MessageText { get; set; }
    public DateTime DateLogged { get; set; }
}

Модульное тестирование

С точки зрения модульного тестирования я хочу убедиться, что HttpClient отправляет указанную полезную нагрузку logEntry на соответствующий URL-адрес (http://www.example.com/api/logging). ( Примечание: я не могу напрямую протестировать метод HttpClient.PostAsync (), потому что мой сервис использует конкретную реализацию HttpClient, а Microsoft не предоставляет для него интерфейс. Однако я могу создать свойсобственный HttpClient, который использует FakeMessageHandler (ниже) в качестве зависимости, и внедрить его в службу для целей тестирования. Оттуда я могу протестировать DoSendAsync ()

//Helper class for mocking the MessageHandler dependency of HttpClient
public abstract class FakeMessageHandler : HttpMessageHandler
{
    protected sealed override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        return DoSendAsync(request);
    }

    public abstract Task<HttpResponseMessage> DoSendAsync(HttpRequestMessage request);
}

Теоретически, я должен бытьвозможность использовать метод Matches () в FakeItEasy для написания пользовательской функции сопоставления. Это будет выглядеть примерно так:

//NUnit Test
[TestFixture]
public class LoggingServiceTests
{
    private LoggingService _loggingService;
    private FakeMessageHandler _fakeMessageHandler;
    private HttpClient _httpClient;

    [SetUp]
    public void SetUp()
    {
        _fakeMessageHandler = A.Fake<FakeMessageHandler>();
        _httpClient = new HttpClient(_fakeMessageHandler);
        _loggingService = new LoggingService(_httpClient);
    }

    [Test]
    public async Task Logs_Error_Successfully()
    {
        var dateTime = new DateTime(2016, 11, 3);
        var logEntry = new LogEntry
        {
            MessageText = "Fake Message",
            DateLogged = dateTime
        };
        await _loggingService.Log(logEntry);

        A.CallTo(() => _fakeMessageHandler.DoSendAsync(
            A<HttpRequestMessage>.That.Matches(
                m => DoesLogEntryMatch("Fake Message", dateTime, HttpMethod.Post,
                    "https://www.example.com/api/logging", m)))
        ).MustHaveHappenedOnceExactly();
    }


    private bool DoesLogEntryMatch(string expectedMessageText, DateTime expectedDateLogged,
        HttpMethod expectedMethod, string expectedUrl, HttpRequestMessage actualMessage)
    {
        //TODO: still need to check expectedMessageText and expectedDateLogged from the HttpRequestMessage content
        return actualMessage.Method == expectedMethod && actualMessage.RequestUri.ToString() == expectedUrl;
    }


}

Проверка URL-адреса и HttpMethod достаточно проста (как показано выше). Но,чтобы проверить полезную нагрузку, мне нужно проверить содержимое HttpRequestMessage. Вот где это становится сложным. Единственный способ, которым я будуБыло обнаружено, что для чтения содержимого HttpRequestMessage используется один из встроенных асинхронных методов (например, ReadAsStringAsync, ReadAsByteArrayAsync, ReadAsStreamAsync и т. д.). Насколько я могу судить, FakeItEasy не поддерживает асинхронные / ожидающие операции внутри соответствий.() предикат.Вот что я попробовал:

  1. Преобразовать метод DoesLogEntryMatch () в асинхронный режим и ожидать вызова ReadAsStringAsync () (НЕ РАБОТАЕТ)

        //Compiler error - Cannot convert async lambda expression to delegate type 'Func<HttpRequestMessage, bool>'.
        //An async lambda expression may return void, Task or Task<T>,
        //none of which are convertible to 'Func<HttpRequestMessage, bool>'
        A.CallTo(() => _fakeMessageHandler.DoSendAsync(
            A<HttpRequestMessage>.That.Matches(
                async m => await DoesLogEntryMatch("Fake Message", dateTime, HttpMethod.Post,
                    "http://www.example.com/api/logging", m)))
        ).MustHaveHappenedOnceExactly();
    
    
    private async Task<bool> DoesLogEntryMatch(string expectedMessageText, DateTime expectedDateLogged,
        HttpMethod expectedMethod, string expectedUrl, HttpRequestMessage actualMessage)
    {
        var message = await actualMessage.Content.ReadAsStringAsync();
        var logEntry = JsonConvert.DeserializeObject<LogEntry>(message);
    
        return logEntry.MessageText == expectedMessageText &&
               logEntry.DateLogged == expectedDateLogged &&
        actualMessage.Method == expectedMethod && actualMessage.RequestUri.ToString() == expectedUrl;
    }
    
  2. Оставьте DoesLogEntryMatch как неасинхронный метод и не ожидайте ReadAsStringAsync ().Кажется, это работает, когда я тестировал его, но я прочитал, что в некоторых ситуациях это может привести к взаимоблокировкам.

    private bool DoesLogEntryMatch(string expectedMessageText, DateTime expectedDateLogged,
        HttpMethod expectedMethod, string expectedUrl, HttpRequestMessage actualMessage)
    {
        var message = actualMessage.Content.ReadAsStringAsync().Result;
        var logEntry = JsonConvert.DeserializeObject<LogEntry>(message);
    
        return logEntry.MessageText == expectedMessageText &&
               logEntry.DateLogged == expectedDateLogged &&
        actualMessage.Method == expectedMethod && actualMessage.RequestUri.ToString() == expectedUrl;
    }
    
  3. Оставьте DoesLogEntryMatch как не асинхронный метод и дождитесь ReadAsStringAsync() внутри Task.Run ().Это порождает новый поток, который будет ожидать результата, но позволяет исходному вызову метода выполняться синхронно.Из того, что я прочитал, это единственный «безопасный» способ вызова асинхронного метода из синхронного контекста (то есть без блокировок).Это то, что я сделал в итоге.

    private bool DoesLogEntryMatch(string expectedMessageText, DateTime expectedDateLogged,
        HttpMethod expectedMethod, string expectedUrl, HttpRequestMessage actualMessage)
    {
        var message = Task.Run(async () => await actualMessage.Content.ReadAsStringAsync()).Result;
        var logEntry = JsonConvert.DeserializeObject<LogEntry>(message);
    
        return logEntry.MessageText == expectedMessageText &&
               logEntry.DateLogged == expectedDateLogged &&
        actualMessage.Method == expectedMethod && actualMessage.RequestUri.ToString() == expectedUrl;
    }
    

Итак, я получил эту работу, но похоже, что в FakeItEasy должен быть лучший способ сделать это.Есть ли что-то эквивалентное методу MatchesAsync (), которое будет принимать предикат, поддерживающий async / await?

1 Ответ

1 голос
/ 06 марта 2019

В FakeItEasy нет MatchesAsync; может быть, это то, что можно добавить (хотя, конечно, это может работать только для асинхронных методов).

Оставьте DoesLogEntryMatch как неасинхронный метод и не ожидайте ReadAsStringAsync (). Кажется, это работает, когда я тестировал его, но я читал, что выполнение этого может вызвать тупики в определенных ситуациях.

На самом деле, я думаю, что это правильный подход. Использование .Wait() или .Result настоятельно не рекомендуется в коде приложения, но вы не в коде приложения, вы в модульном тесте. Это может привести к возникновению тупика, вызванного наличием SynchronizationContext, которое существует в некоторых средах (настольных средах, таких как WPF или WinForms, классический ASP.NET), но не в контексте модульного теста, так что у вас должно быть все в порядке , В прошлом я успешно использовал тот же подход.

...