How to Test Your ASP.NET Core WebApi?
Discover how to test an ASP.NET Core WebApi with Gherkin and Specflow, using readable integration tests and mocks for external services.
I am a backend web developer, and my main task is to create WebApis. Testing is second nature to me, and I use TDD.
Unit tests are extremely helpful for designing software. Unfortunately, they don’t cover everything. They often need to be supplemented with integration tests. This is where BDD comes into play.
These tests are often technical and, in most cases, unreadable to non-developers: try getting your Product Owner or end user to read your code :)
To make the features of a program understandable to them, tests can be written using the Gherkin language.
Gherkin language
Gherkin language allows you to write your tests easily and in a readable way, whether they are unit, integration, or acceptance tests.
The classic format for tests written in Gherkin is:
Feature: Calculator
Scenario: “+” should add to current total
Given the current total is “5”
When I enter “7”
Then the current total should be “12”
@wip
Scenario Outline: Should support
Given the current total is “”
When I enter “”
And I enter “”
Then the current total should be “”
Examples:
| currentTotal | inputNumber | inputOperator | newTotal |
| 5 | 7 | + | 12 |
| 5 | 7 | - | -2 |
| 3 | 4 | * | 12 |
| 9 | 3 | / | 3 |
Gherkin in .Net
The most well-known tool for creating Gherkin tests in a .Net environment is Specflow. We will use it for the remainder of this article.
Use Case: Testing a WebApi
To better understand how to write readable tests and test a WebApi, I will show you how it is possible to test a WebApi in a readable way. This WebApi will need to interface with theMarvel API.
To avoid consuming the Marvel API on every execution, we will simulate calling the API from our WebApi while still allowing it to make real calls to the Marvel API in production.
The WebApi Architecture
The WebApi follows a hexagonal architecture. We have three projects:
- A startup project containing the controllers.
- A project for orchestrating the WebApi (or the business logic in other cases).
- A project for calling the Marvel API.
Test Workflow
Here is the testing plan for the WebApi:
- Create the Gherkin script.
- Set up a server for the WebApi.
- Mock the Marvel web service.
- Call the functionality.
- Test the results of the call
Creating the Gherkin Script
Here is an example of a Gherkin test that is readable by everyone. It describes a feature: character search.
Feature: I want to be able to get information about my favourite Marvel characters
Scenario: I want the list of all superheroes
Given I am a superhero enthusiast
When I request the list of superheroes
Then I get the desired list
Scenario: I want details about a superhero
Given I am a superhero enthusiast
And I want to know more about <Hero>
When I request details about the superhero
Then I get the full character profile
Examples:
| Hero |
| Doctor Strange (Stephen Strange) |
| Star-Lord (Peter Quill) |
| Spider-Man (Peter Parker) |
Setting Up a Server for the WebApi
To test an application, you can use WebApplicationFactory. With this class, it is possible to override the project’s functionality. For example, you can replace the database context or the implementation class for an interface (even one from the framework).
We can create a base class that will be used to call our API:
public class CustomServer : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public HttpClient Client { get; }
public CustomServer(WebApplicationFactory<Startup> factory)
{
_factory = factory;
Client = _factory.CreateClient();
}
}
This class will be provided in the tests usingIClassFixture<CustomServer>
in the class associated with our Gherkin and retrieved in the constructor. To indicate to SpecFlow that the class is linked to a Gherkin file, we can use [Binding]
[Binding]
public class FetchMarvelCharactersSteps : IClassFixture<CustomServer>
{
private readonly CustomServer _server;
public FetchMarvelCharactersSteps(CustomServer server) => _server = server;
[Given("I am a superhero enthusiast)]
public void GivenIAmASuperheroEnthusiast()
{
// Do some stuff
}
// Other tests ...
}
The call to our WebApi is made using the server on the WebApi route:
[Binding]
public class FetchMarvelCharactersSteps : IClassFixture<CustomServer>
{
...
[When("I request the list of superheroes")]
public void ListOfSuperHeroes()
{
var response = await _server.Client.GetAsync(new Uri("/api/characters", UriKind.Relative));
...
}
...
Mocking the Marvel Web Service
The call to the Marvel REST API is made using an HttpClient:
var charactersResponse = await _httpClient.GetAsync(new Uri($"/v1/public/characters?apikey={apiKey}", UriKind.Relative))
This is the call we are going to mock. By mocking this call, we can fully test the WebApi from its own request to the call to the Marvel API.
It's not too complicated once you've done it the first time:
In the CustomServer
class, we will use the RichardSzalay.MockHttp package.
using Moq;
using RichardSzalay.MockHttp;
...
private Mock<IHttpClientFactory> ClientFactory { get; } = new Mock<IHttpClientFactory>();
private MockHttpMessageHandler MessageHandler { get; } = new MockHttpMessageHandler();
public CustomServer(WebApplicationFactory<Startup> factory)
{
_factory = factory;
ClientFactory
.Setup(m => m.CreateClient(It.IsAny<string>()))
.Returns(CreateWebServiceClient);
Client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IHttpClientFactory>();
services.AddScoped(_ => ClientFactory.Object);
});
}).CreateClient();
}
public void SetupRouteResponse(
HttpMethod method,
string route,
HttpResponseMessage response) =>
MessageHandler
.When(method, route)
.Respond(() => Task.FromResult(response));
public void SetupRouteResponse(
HttpMethod method,
string route,
Func<HttpRequestMessage, Task<HttpResponseMessage>> requestHandler) =>
MessageHandler
.When(method, route)
.Respond(requestHandler);
public void SetupRouteResponse(
HttpMethod method,
string route,
HttpResponseMessage response,
params KeyValuePair<string, string>[] query) =>
MessageHandler
.When(method, route)
.WithQueryString(query)
.Respond(() => Task.FromResult(response));
public void SetupRouteResponse(
HttpMethod method,
string route,
Func<HttpRequestMessage, Task<HttpResponseMessage>> requestHandler,
params KeyValuePair<string, string>[] query) =>
MessageHandler
.When(method, route)
.WithQueryString(query)
.Respond(requestHandler);
private HttpClient CreateWebServiceClient()
{
var client = MessageHandler.ToHttpClient();
client.BaseAddress = new Uri("https://marvel.mocked.com");
return client;
}
...
We replace the IHttpClientFactory
with the mock created in the service configuration. Using a lambda in AddScoped
allows routes to be configured afterward as needed.
Above, you can see four methods for mocking the response of the Marvel web service. There are two versions without a query string and two with it. As a reminder, the query string is everything after the ?
in : https://url/api/actions?option1=value1&option2=value2
Si je veux configurer uniquement la réponse j'utilise les méthodes qui prennent un HttpResponseMessage
et si je veux tester la requête envoyée je lui passe une méthode qui fera le contrôle et renverra la réponse configurée.
Example of configuration:
[Binding]
public class FetchMarvelCharactersSteps : IClassFixture<CustomServer>
{
private readonly CustomServer _server;
...
_server.SetupRouteResponse(
HttpMethod.Get,
$"v1/public/characters/{characterId}",
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{ \"code\": \"1234\", ...}", Encoding.UTF8, "application/json")
}
new KeyValuePair<string, string>("apiKey", fakeApiKey)
)
...
}
All that’s left is to call your WebApi, and you can test the entire logic from start to finish.
Conclusion
Combining different types of tests helps you create the right program that does what it is supposed to do. Don’t hesitate to mix them. There are other types of tests as well (such as property-based tests and end-to-end tests).
For Gherkin, I strongly recommend either delegating the writing to someone who can express it functionally or collaborating with such a person.
Gherkin is language-agnostic and can be reused for other technologies like Java, JavaScript, Python, etc.
Additionally, functional documentation can be generated, for example, using Pickles , and presented to any stakeholder to help them understand what the program does.