Patrick Gaule
Patrick Gaule

Reputation: 33

FakeItEasy - Is it possible to test constraints asynchronously (i.e. MatchesAsync)?

I've run into difficulty testing System.Net.Http.HttpClient with FakeItEasy. Consider this scenario:

//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; }
}

Unit Testing

From a unit testing perspective, I want to verify that HttpClient posts the specified logEntry payload to the appropriate URL (http://www.example.com/api/logging). (Side Note: I can't test the HttpClient.PostAsync() method directly because my service uses the concrete implementation of HttpClient and Microsoft does not provide an interface for it. However, I can create my own HttpClient that uses a FakeMessageHandler (below) as a dependency, and inject that into the service for testing purposes. From there, I can test 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);
}

In theory, I should be able to use the Matches() method in FakeItEasy to write a custom matching function. This would look something like this:

//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;
    }


}

Checking the URL and the HttpMethod is easy enough (as demonstrated above). But, in order to check the payload, I need to check the content of the HttpRequestMessage. Here's where it gets tricky. The only way I've found to read the content of an HttpRequestMessage is to use one of the built-in async methods (i.e. ReadAsStringAsync, ReadAsByteArrayAsync, ReadAsStreamAsync, etc.) As far as I can tell, FakeItEasy does not support async/await operations inside of the Matches() predicate. Here's what I tried:

  1. Convert DoesLogEntryMatch() method to async, and await the ReadAsStringAsync() call (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;
    }
    
  2. 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.

    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. Leave DoesLogEntryMatch as a non-async method, and await ReadAsStringAsync() inside of a Task.Run(). This spawns a new thread that will await the result, but allows the original method call to run synchronously. From what I've read, this is the only "safe" way to call an asynchronous method from a synchronous context (i.e. no deadlocks). This is what I wound up doing.

    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;
    }
    

So, I got this working, but it seems like there should be a better way of doing this in FakeItEasy. Is there something equivalent to a MatchesAsync() method that would take a predicate that supports async/await?

Upvotes: 3

Views: 3480

Answers (1)

Thomas Levesque
Thomas Levesque

Reputation: 292425

There's no MatchesAsync in FakeItEasy; maybe it's something that could be added (though of course it could only work for async methods).

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.

In fact, I think that's the correct approach here. Using .Wait() or .Result is strongly discouraged in application code, but you're not in application code, you're in a unit test. The deadlock that can occur is caused by the presence of a SynchronizationContext, which exists in some frameworks (desktop frameworks like WPF or WinForms, classic ASP.NET), but not in the context of a unit test, so you should be fine. I used the same approach successfully in the past.

Upvotes: 1

Related Questions