Integration Tests in ASP.NET Core API
Automated testing is an important part of modern software production as it ensures higher quality and faster delivery, and on the same time it makes testing more affordable. Integration tests, sitting just between Unit Tests and E2E tests, improve test coverage and ensure proper communication between units. In this post, we will explore the possibilities of integration testing of ASP.NET Core by using xUnit to create a test for multiple endpoints.
What is Integration Testing
During the process of creating a smartphone, the display, the sensors, the lenses, the battery, all of the parts, are produced and tested separately (Unit Testing). When two or more units are ready, they are assembled and Integration Testing is performed. This process is similar to all engineering fields, including of course software engineering where integration tests ensure that an app’s components function correctly at a level that includes the app’s supporting infrastructure, such as the database, file system, and network.
ASP.NET Core supports integration tests using a unit test framework with a test web host and an in-memory test server, so some basic understanding of unit tests is assumed. If you are unfamiliar with test concepts and/or xUnit, check the Unit testing C# in .NET Core using dotnet test and xUnit.
What is xUnit
xUnit is the name of a collection of testing frameworks that became very popular among software developers. They derive their structure and functionality from Smalltalk’s SUnit, which was designed by Kent Beck in 1998. Since it was written in a highly structured object-oriented style, it was easy to be adapted to languages such as Java and C#.
You can read more on how to use xUnit in Microsoft Docs or in xUnit Documenation
Creating the test class
Version 2.1 of .NET Core introduced WebApplicationFactory<TEntryPoint> for bootstrapping an application in memory. This factory can be used to create a TestServer instance using the MVC application defined by TEntryPoint
and one or more HttpClient
instances used to send HttpRequestMessage
to the TestServer
. TEntryPoint
is the entry point class of the System Under Test, usually the Startup
class:
public class IntegrationParallelismTesting
{
private readonly WebApplicationFactory<Startup> _factory;
public IntegrationParallelismTesting(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
}
If the SUT’s environment isn’t set, the environment defaults to
Development
.
Since sometimes –like with the WebApplicationFactory<TEntryPoint>
– test context creation and cleanup can be very expensive, the context should be shared across all tests in a class. To achieve this, the test class must implement a class fixture interface (IClassFixture):
public class IntegrationParallelismTesting
: IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public IntegrationParallelismTesting(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
}
Creating a test
This is a trivial task on each own, since Theory
and InlineData
can help create one test for multiple endpoints. The example below uses _factory.CreateClient();
to create an instance of HttpClient
, makes a call to the url
passed as argument, and finally evaluates the results:
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
Using
HttpClient
with theusing
statement can cause a serious issue named ‘sockets exhaustion’. Read more about how to useHttpClientFactory
instead!
Conclusion
Unit Tests vs Integration Tests, the battle continuous as it seems the line is not always so clear: Just replace a mock with the real object and you converted a Unit Test to an Integration Test – not always true…
There is a very nice detailed article by Stevens Anderson called Selective Unit Testing – Costs and Benefits that can help clear things out, but I personally always favor Integration Tests, and write Unit Tests in these cases only:
- Algorithmic logic, e.g. calculations, business rules etc
- Complex code, e.g. code with many dependenciess
- Runtime error prone code, e.g. when using reflection