On va voir comment avoir en quelques minutes des assertions qui vont vérifier les endpoints de notre API avec des scénarios de ce genre:
Feature: Create a new account
As a visitor,
I can create an account to access the game
Scenario: A visitor creates an account
When I fill the login form with
| email | password |
| [email protected] | Jh0nD0e! |
Then I can login with
| email | password |
| [email protected] | Jh0nD0e! |
Et des assertions du style:
[When("I fill the login form with")]
public async Task IFillTheLoginForm(Table table)
{
var row = table.Rows[0];
RegisterUserCommand registerUserCommand = new (row["email"], row["password"]);
var result = await _httpClient.PostAsJsonAsync("/api/users/register", registerUserCommand);
result.EnsureSuccessStatusCode();
}
Si vous utilisez Rider et l’extension Reqnroll, vous pouvez simplement créer un nouveau projet Reqnrol depuis le template et en quelques clics c’est fait !
2. Installation des dépendances
On va rajouter la dépendance à Microsoft.AspNetCore.Mvc.Testing dans notre projet Reqnroll. C’est elle qui va nous permettre d’avoir un client Http très facilement.
II. Configuration
Il va falloir dire qu’on veut lancer le client de test sur notre projet qui contient notre API. Pour se faire, on va dans le program.cs de notre API et on rajoute cette ligne tout à la fin:
public partial class Program { }
On va ensuite créer notre fichier contenant la définition de nos steps (UserStepDefinition.cs dans mon cas) puis initialiser notre client Http via le constructeur de notre classe:
[Binding]
public sealed class UserStepDefinitions
{
private readonly HttpClient _httpClient;
public UserStepDefinitions(WebApplicationFactory<Program> factory)
{
_httpClient = factory.CreateClient(new WebApplicationFactoryClientOptions());
}
}
Et c’est tout ! On peut dès à présent faire des vérifications sur nos endpoints.
Voici ma classe entière qui permet de lancer le scénario présenté en introduction:
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Mvc.Testing;
using Reqnroll;
using StrategyBrowserGameAPI.Command;
namespace AcceptanceTests.StepDefinitions;
[Binding]
public sealed class UserStepDefinitions
{
private readonly HttpClient _httpClient;
public UserStepDefinitions(WebApplicationFactory<Program> factory)
{
_httpClient = factory.CreateClient(new WebApplicationFactoryClientOptions());
}
[When("I fill the login form with")]
public async Task IFillTheLoginForm(Table table)
{
var row = table.Rows[0];
RegisterUserCommand registerUserCommand = new (row["email"], row["password"]);
var result = await _httpClient.PostAsJsonAsync("/api/users/register", registerUserCommand);
result.EnsureSuccessStatusCode();
}
[Then("I can login with")]
public async Task ICanLoginWith(Table table)
{
var row = table.Rows[0];
var loginRequest = new LoginRequest
{
Email = row["email"],
Password = row["password"]
};
var loginResponse = await _httpClient.PostAsJsonAsync("/login", loginRequest);
loginResponse.EnsureSuccessStatusCode();
var accessToken = await ExtractAccessToken(loginResponse);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var manageInfoResult = await _httpClient.GetAsync("/manage/info");
manageInfoResult.EnsureSuccessStatusCode();
}
private static async Task<string> ExtractAccessToken(HttpResponseMessage loginResponse)
{
var loginResponseAsString = await loginResponse.Content.ReadAsStringAsync();
var jsonDoc = JsonDocument.Parse(loginResponseAsString);
var accessToken = jsonDoc.RootElement.GetProperty("accessToken").GetString();
return accessToken;
}
}
Pour aller plus loin
Évidemment, dans mon cas très simple avec une BDD in memory et sans appels externes ça marche très bien mais dans vos applications professionnelles, vous avez sûrement un peu plus de dépendances que ça.
La lib Microsoft.AspNetCore.Mvc.Testing est bien faite et permet facilement d’ajouter de la personnalisation: peut-être que vous souhaitez utiliser une base in-memory pour les tests ou un substitut pour un service externe par exemple.
On ne l’abordera pas dans cet article mais ces exemples sont détaillés dans la doc officielle .Net.
Il arrive souvent que pour tester unitairement des règles de validation, on doive tester le même cas mais avec des exemples différents. Sans tests paramétrés, ça revient à faire un test par cas ce qui peut alourdir notre fichier de tests.
On va voir comment simplifier tout ça avec des tests paramétrés.
Prenons l’exemple d’une classe “Nom de Royaume” où je veux m’assurer ...
Avant .Net 8, tester du code qui utilise DateTime.Now() n’était pas trivial, on devait faire en sorte de mocker la Clock dans nos tests. Depuis .Net 8, c’est beaucoup plus facile grâce à TimeProvider inclut par défaut !
En deux mots, TimeProvider est une...
La gestion des erreurs de son API est très importante pour que les consommateurs puissent avoir une description claire du problème mais c’est souvent fastidieux à maintenir.
On va voir comment avoir des statuts de réponse cohérent et des messages d’erre...
Depuis .NET 9, le le support d’OpenAPI est directement inclus dans .NET et ne passe plus par les librairies Swagger par défaut (plus d’info sur ce choix ici si jamais ça vous intéresse).
De façons simplifiée, la librairie Swashbuckle.AspNetCore.Sw...