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

时间:2019-03-05 17:43:24

标签: c# asynchronous async-await dotnet-httpclient fakeiteasy

我遇到了使用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没有为此提供接口。但是,我可以创建自己的使用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()谓词内部的操作。这是我尝试过的:

  1. 将didLogEntryMatch()方法转换为异步方法,然后等待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. 将DidLogLogryryMatch保留为非异步方法,并且不等待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. 将didLogEntryMatch保留为非异步方法,并在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()方法等效的东西,该方法采用支持异步/等待的谓词?

1 个答案:

答案 0 :(得分:1)

FakeItEasy中没有MatchesAsync;也许可以添加一些内容(尽管它当然仅适用于异步方法)。

  

将DidLogLogryryMatch保留为非异步方法,并且不等待ReadAsStringAsync()。当我对其进行测试时,这似乎可行,但是我已经读到,这样做可能在某些情况下导致死锁。

实际上,我认为这是正确的方法。在应用程序代码中强烈建议不要使用.Wait().Result,但您不在应用程序代码中,而是处于单元测试中。可能发生的死锁是由SynchronizationContext的存在引起的, Picasso.with(this) .load(TAG_IMAGE) .error(R.mipmap.no_image_available) .into(imagePhoto); 存在于某些框架(如WPF或WinForms的桌面框架,经典的ASP.NET)中,但是在单元测试的上下文中不存在,因此您应该没事的。我过去曾经成功地使用过相同的方法。