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:
- The user calls the endpoint /api/v1/movies.
- The application will do fake processing.
- 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.
Top comments (5)
Very insightful!
Did not know that there exists such a integration testing library already. I created my own - but it works quite the same.
Itâs called integration testing, not unit test anymore. The benefit of it, it simulates API call. The cons, not all code coverage tool can identify it. So we need to figure out our self or it will remain uncovered.
Hi David! Actually, I had to create my own as well. Basically, I needed many customizations, like creating a database to handle parallel testing for each method test. So I wrote down a bit of the detail regarding it in this Tic Tac Toe through API repository.
Oh, glad to hear... thought of much unnecessary work done upfront đ
You donât need to remove previous registration to mock. I always do this in my project.