C# Web API: How to call your endpoint through integration tests

It's fantastic when you have all of your unit tests returning green signs everywhere. Still, when you execute your project, it raises an error because an infrastructure setup is wrong, making it impossible to run your Web API properly πŸ˜‘. It can be how you set up your logs, database, providers, well, many other things. One approach to fix it or minimize its impact is through integration tests. So how can you do a quick setup for that πŸ€”? By the way, in this blog post, we're going to consider the following technologies:

Describing the sample project

Here's the image that shows a sample flow:

It is composed of three steps:

  1. The user calls the endpoint /api/v1/movies.
  2. The application will do fake processing.
  3. A random movie is returned to the user.

To take care of this business rule, here's our controller:

[ApiController]
[Route("api/v1/[controller]")]
public class MoviesController : ControllerBase
{
    private readonly IFilmSpecialist _filmSpecialist;

    public MoviesController(IFilmSpecialist filmSpecialist)
    {
        _filmSpecialist = filmSpecialist;
    }

    [HttpGet]
    public Movie Get()
    {
        Log.Information("Let me ask the film specialist...");
        var movie = _filmSpecialist.SuggestSomeMovie();
        Log.Information("Suggested movie: {Movie}", movie);
        return movie;
    }
}

Who will be responsible for doing fake processing:

public class FilmSpecialist : IFilmSpecialist
{
    private static readonly Movie[] Films =
    {
        new("RoboCop", "10/08/1987", new[] {"Action", "Thriller", "Science Fiction"}, "1h 42m"),
        new("The Matrix", "05/21/1999", new[] {"Action", "Science Fiction"}, "2h 16m"),
        new("Soul", "12/25/2020", new[] {"Family", "Animation", "Comedy", "Drama", "Music", "Fantasy"}, "1h 41m"),
        new("Space Jam", "12/25/1996", new[] {"Adventure", "Animation", "Comedy", "Family"}, "1h 28m"),
        new("Aladdin", "07/03/1993", new[] {"Animation", "Family", "Adventure", "Fantasy", "Romance"}, "1h 28m"),
        new("The World of Dragon Ball Z", "01/21/2000", new[] {"Action"}, "20m"),
    };

    public Movie SuggestSomeMovie()
    {
        Log.Debug("OKAY! Which film will I suggest πŸ€”");
        Random random = new();
        var filmIndexThatIWillSuggest = random.Next(0, Films.Length);
        Log.Information("Will suggest the film with index {FilmIndex}!", filmIndexThatIWillSuggest);

        return Films[filmIndexThatIWillSuggest];
    }
}

Let's first do the unit testing to ensure that our methods contracts are being respected.

Starting from unit testing

Here we'll do a simple unit test on the service responsible for returning a random movie. We can write something like the following, as it's not our focus:

public class FilmSpecialistTests
{
    private readonly IFilmSpecialist _filmSpecialist = new FilmSpecialist();

    [Fact]
    public void ShouldReturnRandomMovieWhenAsked()
    {
        // Act
        var suggestedMovie = _filmSpecialist.SuggestSomeMovie();
        // Assert
        var expectedTiles = new[]
        {
            "RoboCop", "The Matrix", "Soul", "Space Jam", "Aladdin", "The World of Dragon Ball Z"
        };
        suggestedMovie.Title.Should().BeOneOf(expectedTiles);
    }
}

Making an actual HTTP request to our API

To call our endpoint, we can use a class fixture with the help of WebApplicationFactory (know more about it at the section Basic tests with the default WebApplicationFactory in Integration tests in ASP.NET Core guide). In our class test constructor, we can use the factory to create a HttpClient, hence allowing us to do HTTP calls to our endpoint. Moreover, let's say you'd like to replace an injected service with a mock: you can do that through ConfigureTestServices. To illustrate a complete example:

public class MoviesControllerITests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly IFilmSpecialist _filmSpecialist;
    private HttpClient _httpClient;

    public MoviesControllerITests(WebApplicationFactory<Startup> factory)
    {
        _filmSpecialist = Mock.Of<IFilmSpecialist>();
        _httpClient = factory.WithWebHostBuilder(builder =>
        {
            // https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-5.0#inject-mock-services
            builder.ConfigureTestServices(services =>
            {
                services.RemoveAll<IFilmSpecialist>();
                services.TryAddTransient(_ => _filmSpecialist);
            });
        }).CreateClient();
    }

    [Fact]
    public async Task ShouldCreateGameGivenFirstMovementIsBeingExecuted()
    {
        // Arrange
        var requestPath = "/api/v1/movies";
        var movieToBeSuggested = new Movie("Schindler's List", "12/31/1993", new[] {"Drama", "History", "War"}, "3h 15m");
        Mock.Get(_filmSpecialist)
            .Setup(f => f.SuggestSomeMovie())
            .Returns(movieToBeSuggested)
            .Verifiable();
        // Act
        var response = await _httpClient.GetAsync(requestPath);
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var movie = await response.Content.ReadFromJsonAsync<Movie>();
        // Assert
        movie.Should().BeEquivalentTo(movieToBeSuggested);
        Mock.Get(_filmSpecialist).Verify();
    }
}

By the way, to use WebApplicationFactory, you must install the package:

Microsoft.AspNetCore.Mvc.Testing

It's pretty simple πŸ€—. I'll leave it as it is, but we could abstract our integration test to avoid creating a HttpClient every time for each of our class tests 😏.

To end things off

I think you can get enormous benefits from doing a cheap integration test sometimes because, as I told you at the beginning, it can almost guarantee that your code will be shipped as expected at the infrastructure layer. In this article, I gave a somewhat simple example, but things can be more challenging, let's say when it comes to broker connection β€” the subject of another blog entry 😜.

You can consult the code I showed here on this GitHub repository. You can use Docker Compose to run the project as well as execute its tests. Check the README for more details.

Posted listening to Toy Soldiers, Martika.

21