This post aims to show alternative approach to functional API testing. ASP.NET Core ships with great TestServer class, which tremendously eases any kind of functional and integration testing on the platform. Most of the examples of testing APIs you’ll find follows one of the two approaches: either unit testing controller or using TestServer, where we create HTTP request, send it over and assert the response.

Lets go throught each of them and see what they offer.

Unit testing controllers

The example is very straightforward and there isn’t much to talk about here. We set up our test subject with a mocked repository, call the actual method on a controller and assert the view model returned from our invocation.

[Fact]
public async Task it_returnes_the_list_of_venues_when_calling_index_method()
{
    // Arrange
    var mockRepo = new Mock<IVenueRepository>();
    mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(GetVenueTestFixtures()));
    var controller = new VenueController(mockRepo.Object);

    // Act
    var result = await controller.Index();

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsAssignableFrom<IEnumerable<VenueViewModel>>(
        viewResult.ViewData.Model);
    Assert.Equal(10, model.Count());
}

In my personal opinion, there isn’t much too gain from this kind of testing. The controller usually sits at one of the highest layers, the User Interface Layer. The further we go up through the layers from Domain up to the User Interface, the value of pure unit tests itself diminishes. Like the test above gives us no confidence that our application works at all. We don’t check if our dependencies are wired up properly, if the request was being translated properly, or if the response was sent to the end user properly. We don’t even check which fields we exposed as part of the public API and which we didn’t.

In light of those observations, this second approach seems much more reasonable.

Integration testing using TestServer

Let’s see how our examples with venues will look up using the TestServer.

public class VenueControllerIntegrationTest
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    private readonly TestState _state;

    public VenueControllerIntegrationTest()
    {
        _server = new TestServer(new WebHostBuilder()
            .ConfigureServices(services => {
                services.AddSingleton<TestState>();
            })
            .UseStartup<Startup>());
        _client = _server.CreateClient();
        _state = _server.Host.Services.GetService<TestState>();
    }

    [Fact]
    public async Task test_listing_of_venues()
    {
        // Arrange
        _state.VenueExists(Guid.NewGuid(), ".NET meetup Manhattan", "Broadway St.");
        _state.VenueExists(Guid.NewGuid(), ".NET meetup Cracov", "Florianska");

        // Act
        var response = await _client.GetAsync("/");
        response.EnsureSuccessStatusCode();

        var responseString = await response.Content.ReadAsStringAsync();

        // Assert
        var venues = JsonConvert.DeserializeObject<IEnumerable<VenueViewModel>>(responseString);
        venues.Count().Should().Be(2);
    }
}

This high level test already gives us more confidence, by making sure the dependencies are wired up properly during runtime, our request was translated by the server and the response was successful.

We’re left with a small issue. Does this test really serve as great documentation? The assert phase only checks if the value was deserialized properly into view model. If we would like to figure out the actual response, we would have to drill down into view model and figure out how its being serialized for the response and fit the pieces together in our head.

This is where the NMatcher library comes in play. Lets rework our test case in the next section while expanding our case with an additional one for the creation of the venue.

Functional testing using TestServer and NMatcher

We can use NMatcher to assert the proper JSON and leave out any application specific details when testing our path from Request to Response. This make our test fully functional, as we take the raw input and assert the raw output. This way we make sure every piece of code responsible for manipulating the HTTP pipeline was hit, all request translations were done properly and the response output is what we expect. It also serves as great documentation for any developer that wants to use our API (and for ourselves as well). We also can use its functionality to omit some parts of the response we don’t care about.

[Fact]
public async Task test_listing_of_venues()
{
    // Arrange
    _state.VenueExists(Guid.NewGuid(), ".NET meetup Manhattan", "NY, Broadway St.");
    _state.VenueExists(Guid.NewGuid(), ".NET conference", "Cracov, Florianska");

    // Act
    var response = await _client.GetAsync("/");
    response.EnsureSuccessStatusCode();

    // Assert
    var responseBody = await response.Content.ReadAsStringAsync();
    responseBody.Should().MatchJson(@"[
        {
            ""VenueId"":""@guid@"",
            ""Name"":"".NET meetup Manhattan"",
            ""Address"":""NY, Broadway St."",
            ""CreatedAt"": ""@string@.IsDateTime()""
        },
        {
            ""VenueId"":""@guid@"",
            ""Name"":"".NET conference"",
            ""Address"": ""Cracov, Florianska"",
            ""CreatedAt"": ""@string@.IsDateTime()""
        }
    ]");
}

[Fact]
public async Task test_creation_of_venues()
{
    var payload = @"
        {
            ""Name"": "".NET meetup Manhattan"",
            ""Address"": ""NY, Broadway St."",
            ""Seats"": ""10"",
            ""DiscountCoupons"": [
                {""CouponCode"": ""PC0001"", ""ProductName"": ""Awesome PC"" },
                {""CouponCode"": ""PC0002"", ""ProductName"": ""Awesome PC #2"" }
            ]
        }
    ";
    var response = await af.PostJson("api/venues", payload);
    response.StatusCode.Should().Be(HttpStatusCode.Created);

    var responseBody = await response.Content.ReadAsStringAsync();

    responseBody.Should().MatchJson(@"
        {
            ""VenueId"":""@guid@"",
            ""Name"":"".NET meetup Manhattan"",
            ""Address"":""NY, Broadway St."",
            ""CreatedAt"": ""@string@.IsDateTime()""
        }
    ");

}

As we can see, in order to make our testing easier, we introduced the notion of matchers, which serves as a container for values that we cannot predict. Those values can be guids, auto increment ids, dates, times etc.

Check out the NMatcher github page for full documentation and what it can do for You. Play around with the alternative approach to testing APIs with ASP.NET Core and perhaps it will fit your needs, as it fit mine.