In this article, I will not delve into testing principles, as there are already numerous resources available on that topic. Instead, I will share a test strategy based on my own experience for early and frequent testing of microservices with external service connectivity within a .NET development environment.
In many microservice-based projects, integration testing becomes significantly more challenging when a microservice is part of a distributed system and interacts with other microservices, whether they are within or outside the organization’s control. This often leads to shifting the testing of related functionality to end-to-end testing, which can place additional pressure on QA during this phase and violate testing principles such as shift-left and “F.I.R.S.T.” (Robert C. Martin – 2008 – “Clean Code: A Handbook of Agile Software Craftsmanship” – ISBN: 9780359582266).
Testing microservices in isolation that connect to external services can be managed by setting up Docker containers for these connections. However, this approach adds significant complexity to test projects and makes debugging failed test cases more difficult. Maintaining these Docker containers also requires resources and may be one of the first tasks to be neglected when a project is under pressure, such as from deadlines. This complexity can also make it harder for new team members to integrate into the project. Additionally, new defects can be easily introduced as developers may take shortcuts in testing due to the complexity of the test project or strategy. This approach also does not address testing connectivity to external services outside the development team’s or organization’s control.
As described in the article Integration tests in ASP.NET Core, microservice testing can be conducted early in the development stage as part of a test project using a WebApplicationFactory
. However, this method does not cover connectivity to external services. Most articles on this topic describe integration testing as outlined in the aforementioned Microsoft article, still leaving out external service connectivity. A more thorough search may lead to articles on contract testing, such as Consumer-Driven Contract Testing (CDC). This article provides a good overview of integration testing with external service connectivity and test frameworks that can be used. However, the mentioned frameworks (Pact and Spring Cloud Contract) are not native to .NET and tend to be complex, making their implementation in a test project difficult and hard to maintain. Examples of Spring Cloud Contract in C# are hard to find, and Pact cannot be integrated with the WebApplicationFactory
.
Recently, I developed a library to address the essential needs of consumer-driven contract testing in .NET. I released it as a NuGet package named Cympatic.Extensions.Stub. This package, compatible with .NET 6.0 and higher, can be seamlessly integrated into a custom WebApplicationFactory
. It offers an in-memory test web host (StubServer
) for external services, providing configurable responses to requests made to these services. Each request is recorded and can be validated as part of integration tests.
Usage
To use the StubServer
, start by referencing the Microsoft.AspNetCore.Mvc.Testing and Cympatic.Extensions.Stub packages in your test project. Then, expose the implicitly defined Program class of the microservice project to the test project by doing one of the following:
-
Expose all internal types from the web app to the test project. Add the following to the microservice project’s
.csproj
file:
<ItemGroup>
<InternalsVisibleTo Include="MyIntegrationTests" />
</ItemGroup>
-
Make the
Program
class public by adding a partial class declaration at the end of theProgram.cs
file in the microservice project:
public partial class Program { }
Setup
To set up the StubServer
in a custom WebApplicationFactory
, follow these steps:
-
Initialize the
StubServer
in the constructor of your customWebApplicationFactory
:
_stubServer = new StubServer();
-
Add proxy methodes for adding responses to the
StubServer
:
public Task<ResponseSetup> AddResponseSetupAsync(ResponseSetup responseSetup, CancellationToken cancellationToken = default)
=> _stubServer.AddResponseSetupAsync(responseSetup, cancellationToken);
public Task AddResponsesSetupAsync(IEnumerable<ResponseSetup> responseSetups, CancellationToken cancellationToken = default)
=> _stubServer.AddResponsesSetupAsync(responseSetups, cancellationToken);
-
Add proxy methods for reading requests from the
StubServer
:
public Task<IEnumerable<ReceivedRequest>> FindReceivedRequestsAsync(ReceivedRequestSearchParams searchParams, CancellationToken cancellationToken = default)
=> _stubServer.FindReceivedRequestsAsync(searchParams, cancellationToken);
-
Add proxy methods for removing responses and received requests from the
StubServer
:
public Task ClearResponsesSetupAsync(CancellationToken cancellationToken = default)
=> _stubServer.ClearResponsesSetupAsync(cancellationToken);
public Task ClearReceivedRequestsAsync(CancellationToken cancellationToken = default)
=> _stubServer.ClearReceivedRequestsAsync(cancellationToken);
-
Override the
Dispose
method since theStubServer
is a disposable object:
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_stubServer.Dispose();
}
}
-
Override the
CreateHost
method of the WebApplicationFactory to configure the base address of the used external service:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) =>
{
context.Configuration["ExternalApi"] = _stubServer.BaseAddressStub.ToString();
});
base.ConfigureWebHost(builder);
}
For the full source file of a custom WebApplicationFactory
, you can refer to this example.
Usage in a Test
Create a test class that implements a IClassFixture<>
interface, referencing the custom WebApplicationFactory
to share object instances across all tests within the class.
public class WeatherForecastTests : IClassFixture<ExampleWebApplicationFactory<Program>>
{
private readonly ExampleWebApplicationFactory<Program> _factory;
private readonly HttpClient _httpClient;
public WeatherForecastTests(ExampleWebApplicationFactory<Program> factory)
{
_factory = factory;
_httpClient = _factory.CreateClient();
_factory.ClearResponsesSetupAsync();
_factory.ClearReceivedRequestsAsync();
}
}
A typical test uses the factory to set up the response and process the request through the HttpClient
. The request to the external service can then be validated.
[Fact]
public async Task GetAllWeatherForecasts()
{
static IEnumerable<WeatherForecast> GetItems()
{
for (var i = 0; i < NumberOfItems; i++)
{
yield return GenerateWeatherForecast(i);
}
}
// Arrange
var expected = GetItems().ToList();
var responseSetup = new ResponseSetup
{
Path = "/external/api/weatherforecast",
HttpMethods = [HttpMethod.Get.ToString()],
ReturnStatusCode = HttpStatusCode.OK,
Response = expected
};
await _factory.AddResponseSetupAsync(responseSetup);
var expectedReceivedRequests = new List<ReceivedRequest>
{
new(responseSetup.Path, responseSetup.HttpMethods[0], responseSetup.Query, responseSetup.Headers, string.Empty, true)
};
// Act
var response = await _httpClient.GetAsync("/weatherforecast");
// Assert
var actual = await response.Content.ReadFromJsonAsync<IEnumerable<WeatherForecast>>();
actual.Should().BeEquivalentTo(expected);
var actualReceivedRequests = await _factory.FindReceivedRequestsAsync(new ReceivedRequestSearchParams("/external/api/weatherforecast", [HttpMethod.Get.ToString()]));
actualReceivedRequests.Should().BeEquivalentTo(expectedReceivedRequests, options => options
.Excluding(_ => _.Headers)
.Excluding(_ => _.Id)
.Excluding(_ => _.CreatedDateTime));
}
-
Prepare the
ResponseSetup
:- Set
Path
andHttpMethods
to a partial path and HttpMethod of the expected request used by the external service. - Set
ReturnStatusCode
to the desiredHttpStatusCode
. - Set
Response
to the desired response from the external service.
- Set
-
Add the
ResponseSetup
to theStubServer
using theAddResponseSetupAsync
method.
TIP: You can add multiple
ResponseSetup
instances in a single call using theAddResponsesSetupAsync
method.
-
Process the request to the System Under Test (SUT) using the
HttpClient
. - Verify the response from the SUT.
-
Verify the request made to the external service:
- Use the
FindReceivedRequestsAsync
method to locate the request made to the external service. The request can be found based on a combination ofPath
,HttpMethod
, andQuery
.
- Use the
NOTE:
ReceivedRequest
can only be found when there is a matchingResponseSetup
.
Happy coding and testing! 🚀
Top comments (0)