FakeItEasy - 是不是可以异步测试约束(即 MatchesAsync)?

Posted

技术标签:

【中文标题】FakeItEasy - 是不是可以异步测试约束(即 MatchesAsync)?【英文标题】:FakeItEasy - Is it possible to test constraints asynchronously (i.e. MatchesAsync)?FakeItEasy - 是否可以异步测试约束(即 MatchesAsync)? 【发布时间】:2019-03-05 17:43:24 【问题描述】:

我在使用 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 不支持异步/等待Matches() 谓词内的操作。这是我尝试过的:

    将 DoesLogEntryMatch() 方法转换为异步,并等待 ReadAsStringAsync() 调用 (DOES NOT WORK)

        //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()。这会产生一个新线程来等待结果,但允许原始方法调用同步运行。根据我的阅读,这是从同步上下文调用异步方法的唯一“安全”方式(即没有死锁)。这就是我最终要做的。

    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 的谓词?

【问题讨论】:

你看过TestServer(docs.microsoft.com/en-us/previous-versions/aspnet/…,docs.microsoft.com/en-us/dotnet/api/…)吗? 【参考方案1】:

FakeItEasy 中没有MatchesAsync;也许可以添加一些东西(当然它只能用于异步方法)。

将 DoesLogEntryMatch 保留为非异步方法,并且不要等待 ReadAsStringAsync()。这在我测试时似乎有效,但我读到这样做可能会在某些情况下导致死锁。

事实上,我认为这是正确的做法。强烈建议不要在应用程序代码中使用.Wait().Result,但您不在应用程序代码中,而是在单元测试中。可能发生的死锁是由 SynchronizationContext 的存在引起的,它存在于某些框架中(桌面框架如 WPF 或 WinForms,经典 ASP.NET),但在单元测试的上下文中不存在,所以你应该没问题.我过去成功地使用了相同的方法。

【讨论】:

以上是关于FakeItEasy - 是不是可以异步测试约束(即 MatchesAsync)?的主要内容,如果未能解决你的问题,请参考以下文章

FakeItEasy DbSet / IQueryable<T> - 实体框架 6

使用FakeItEasy声明事件已被引发

c#单元测试:使用Moq框架Mock对象

在单元测试中同步调用异步方法是不是不正确?

如何对 IUIVisualizerService 的调用进行单元测试

使用 Mocha 测试是不是抛出异步函数