FakeItEasy - 是否可以异步测试约束(即 MatchesAsync)?
FakeItEasy - Is it possible to test constraints asynchronously (i.e. MatchesAsync)?
我 运行 难以使用 FakeItEasy 测试 System.Net.Http.HttpClient。考虑这种情况:
//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);
}
理论上,我应该可以使用FakeItEasy中的Matches()方法来编写自定义匹配函数。这看起来像这样:
//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 不支持 async/await Matches() 谓词内部的操作。这是我尝试过的:
将 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 保留为非异步方法,并在 Task.Run() 内等待 ReadAsStringAsync()。这会生成一个等待结果的新线程,但允许同步调用 运行 的原始方法。据我所知,这是从同步上下文调用异步方法的唯一 "safe" 方法(即没有死锁)。这就是我最后要做的。
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 的谓词?
FakeItEasy 中没有MatchesAsync
;也许它是可以添加的东西(当然它只能用于异步方法)。
Leave DoesLogEntryMatch as a non-async method, and don't await ReadAsStringAsync(). This seems to work when I tested it, but I have read that doing this could cause deadlocks in certain situations.
事实上,我认为这是正确的做法。强烈建议不要在应用程序代码中使用 .Wait()
或 .Result
,但你不是在应用程序代码中,而是在单元测试中。可能发生的死锁是由SynchronizationContext
的存在引起的,它存在于某些框架中(桌面框架如WPF或WinForms,经典ASP.NET),但不在单元测试的上下文中,所以你应该没事。我过去成功地使用了相同的方法。
我 运行 难以使用 FakeItEasy 测试 System.Net.Http.HttpClient。考虑这种情况:
//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);
}
理论上,我应该可以使用FakeItEasy中的Matches()方法来编写自定义匹配函数。这看起来像这样:
//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 不支持 async/await Matches() 谓词内部的操作。这是我尝试过的:
将 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 保留为非异步方法,并在 Task.Run() 内等待 ReadAsStringAsync()。这会生成一个等待结果的新线程,但允许同步调用 运行 的原始方法。据我所知,这是从同步上下文调用异步方法的唯一 "safe" 方法(即没有死锁)。这就是我最后要做的。
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 的谓词?
FakeItEasy 中没有MatchesAsync
;也许它是可以添加的东西(当然它只能用于异步方法)。
Leave DoesLogEntryMatch as a non-async method, and don't await ReadAsStringAsync(). This seems to work when I tested it, but I have read that doing this could cause deadlocks in certain situations.
事实上,我认为这是正确的做法。强烈建议不要在应用程序代码中使用 .Wait()
或 .Result
,但你不是在应用程序代码中,而是在单元测试中。可能发生的死锁是由SynchronizationContext
的存在引起的,它存在于某些框架中(桌面框架如WPF或WinForms,经典ASP.NET),但不在单元测试的上下文中,所以你应该没事。我过去成功地使用了相同的方法。