Я столкнулся с трудностями при тестировании 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 не поддерживает асинхронные / ожидающие операции внутри соответствий.() предикат.Вот что я попробовал:
Преобразовать метод 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;
}
Оставьте 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;
}
Оставьте 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?