Appréhender les tests avec xUnit

Les tests sont un moyen incontournable pour un projet de vérifier que le code est toujours fonctionnel et qu’il ne comporte pas de bug lors du développement. Dans un esprit d’intégration continue et de build automatique de la solution, les tests sont là afin de valider que chaque nouveau code n’a pas intégré de nouveaux bugs dans le système.

ASP.NET Core a été conçu dans le but d’améliorer la testabilité du code de l’utilisateur et ainsi augmenter la maintenabilité de l’application. Les tests automatisés sont ainsi un bon moyen pour le développeur de vérifier que le code qu’il a écrit fait bien ce qu’il doit faire. Il existe un bon nombre de type de tests, comme les tests d’intégration, les tests de montés en charge, les tests fonctionnels et ben d’autres encore. Les tests qui nous intéressent dans cette partie concernent les tests unitaires, c’est-à-dire des tests courts et simples permettant de valider des petits morceaux de code.

Ecrire des tests unitaires n’est pas si simple, et le développeur doit bien concevoir ses classes, ses méthodes et découpler le plus possible son architecture afin de rendre ses tests les plus simples possible. Les dépendances d’une classe peuvent être occultées en suivant le principe Explicit Dependencies Principle et en utilisant ainsi l’injection de dépendance : le découplage est alors assuré.

Une bonne pratique est de séparer les tests dans un projet à part du projet contenant les classes. Un projet de test est simplement une librairie de classe référençant un lanceur de tests (un test runner) et le projet à tester (intitulé le System Under Test, ou SUT). La convention d’ASP.NET va même plus loin en séparant les projets de tests et les projets SUT dans des dossiers à parts :

global.json
MonProjet.sln
src/
  MonProjet/
    project.json
    Startup.cs
    Services/
      MonService.cs
test/
  MonProjet.UnitTests/
    project.json
    Services/
      MonService_Test.cs

Un projet de test doit d’abord être configuré afin d’indiquer quel framework de test il doit utiliser. Le framework conseillé par l’équipe d’ASP.NET Core est xUnit. Il suffit alors de le rajouter dans le projet de test via NuGet. Cela nous donne dans le .csproj :

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="xunit" Version="2.3.0-beta2-build3683" />
    <DotNetCliToolReference Include="dotnet-xunit" Version="2.3.0-beta2-build3683" />
  </ItemGroup>

</Project>

La première dépendance permet évidemment de tester les classes dans notre projet SUT. La deuxième dépendance permet d’ajouter le test runner au projet de test. Enfin, la dernière dépendance permet de lancer les tests avec la ligne de commande dotnet test.

Il existe plusieurs types de tests avec xUnit :

  • Facts : tests qui sont toujours vrai et qui testent des conditions invariantes ;
  • Theroy : tests qui ne sont vrai que pour un jeu de donnée bien particulier.

Les Facts sont des tests très simples permettant de tester des égalités, des conditions ou encore des exceptions mais de manière fixe : le test n’est pas « paramétrable ».

[Fact]
public void PassingTest()
{
    Assert.Equal(4, Add(2, 2));
}

En revanche, les théories permettant d’injecter des valeurs et ainsi vérifier des jeux de données bien particuliers. La théorie ci-dessous va vérifier que la méthode CheckValeur retourne bien false pour les valeurs -1, 0 et 1.

[Theory]
[InlineData(-1)]
[InlineData(0)]
[InlineData(1)]
public void ReturnFalseGivenValues(int value)
{
    var result = _monService.CheckValeur(value);

    Assert.False(result);
}

Les tests peuvent être lancés directement dans Visual Studio via le Test Explorer. Cet onglet permet au développeur de se munir d’un outil graphique afin de lancer les tests, visualiser les erreurs levées durant le test, vérifier la couverture du code et ainsi de suite.

Test Explorer de Visual Studio

L’onglet permet de visualiser :

  • Les tests réussis ;
  • Les tests qui ont échoués ;
  • Les erreurs survenues pendant le déroulement du test, avec une stacktrace détaillée de l’exception ;
  • Le regroupement des tests suivant la classe où ils sont définit. Le nombre de test définis dans un projet peut être très important, il est donc important de nommer et regrouper ses tests de manière à les retrouver facilement dans le Test Explorer.
  • La couverture de code. Visual Studio permet de lancer une analyse de couverture de code afin de déterminer combien de pourcentage de code a été utilisé lors du test sélectionné. Cela permet rapidement de vérifier si tous les cas possibles ont été testés.

Il est également possible de lancer les tests via l’outil en ligne de commande dotnet test (pour la RC2) ou dnx test (pour la RC1). L’utilitaire va alors chercher tous les tests unitaires et les lancées dans la foulée.

Résultat des tests lancés via ligne de commande

Il est cependant fastidieux de tout le temps lancer les tests à la main lors de chaque modification. L’utilitaire intègre un système permettant de scruter les fichiers et lancer les tests automatiquement lorsque l’un d’eux a été modifié grâce à la commande dotnet watch test.

L’intérêt de l’automatisation des tests est que cela permet d’immédiatement visualiser le résultat et ainsi résoudre rapidement les possibles bugs qui ont été introduits via l’ajout de nouvelles fonctionnalités. Ce processus est également facilement utilisable via l’intégration continue, permettant ainsi à un serveur de lancer les tests de manière automatique lors de chaque commit d’un développeur.

Les tests unitaires se décomposent souvent en 3 phases qui forment le pattern intitulé Arrange-Act-Assert (AAA). Ce dernier possède les caractéristiques suivantes :

  • Arrange : c’est la phase d’initialisation des objets du test. En effet, un test à souvent besoin d’un contexte, et d’un (ou un ensemble) objet pour effectuer le test. La phase Arrange se fait au début du lancement du test, mais il est possible de trouver deux phases Arrange différentes. La première dans le constructeur de la classe de test permettant d’initialiser les objets globaux pour tous les tests. Une seconde phase Arrange peut s’effectuer dans la méthode de test en elle-même afin d’initialiser des variables propres au test.
  • Act : c’est la phase d’exécution du test. Cette phase appelle le code nécessaire pour valider le test et récupère les résultats escomptés. C’est ici que sont créés les paramètres servant au test. Le mieux est de créer une méthode par test.
  • Assert : c’est la phase de vérification des résultats. Cette phase s’appuie sur une API d’assertion souvent fourni par les frameworks de test unitaire permettant de vérifier les égalités, les conditions, les exceptions … De plus, elle s’exécute à la suite de la phase Act et donc dans chaque méthode de test.

Le framework xUnit permet également de lancer des tests d’intégration via les mêmes outils précédemment cités.

Un test d’intégration est simplement un test qui imbrique plusieurs composants du système (infrastructure, base de données …) pour vérifier qu’ils fonctionnent correctement ensemble. C’est l’inverse d’un test unitaire qui lui ne doit absolument pas prendre en compte les autres composants du système. Il est souvent d’usage de séparer ces deux types de tests dans des projets séparés.

Les tests unitaires utilisent généralement des mock (expliqué dans la section suivante) afin d’imiter le comportement d’un autre composant (une base de donnée par exemple), alors que le but des tests d’intégration est d’utiliser les mêmes objets qui seront utilisés en production.

ASP.NET Core permet de simuler un serveur en mémoire et un client fictif afin de lancer des requêtes au serveur et de vérifier que la réponse est la bonne. Cela est particulièrement pratique pour tester que le pipeline HTTP et l’enchaînement des middlewares se fait correctement. Afin d’utiliser ce composant, il faut ajouter la référence ‘Microsoft.AspNetCore.TestHost’: ‘1.1.2’. L’exemple de code ci-dessous définit un <host fictif afin de vérifier que la réponse est bien conforme à ce qui est attendu.

private readonly TestServer _server;
private readonly HttpClient _client;

public MonTest()
{
    // Arrange
    _server = new TestServer(TestServer.CreateBuilder()
        .UseStartup<Startup>());

    _client = _server.CreateClient();
}

[Fact]
public async Task ReturnHelloWorld()
{
    // Act
    var response = await _client.GetAsync("/");
    response.EnsureSuccessStatusCode();

    var responseString = await response.Content.ReadAsStringAsync();

    // Assert
    Assert.Equal("Hello World!", responseString);
}

Il a plusieurs manières de configurer un TestServer. Dans l’exemple ci-dessus, le serveur est configuré de la même manière que notre projet SUT via la classe Startup, mais il est possible d’indiquer uniquement les services que l’on veut tester.

Une autre pratique des tests unitaires est de tester la logique des contrôleurs MVC. Le but de ces tests est de vérifier que le code du contrôleur effectue les bonnes vérifications et interactions avec les services dont il a besoin. Un contrôleur ne doit pas effectuer de code métier et doit rester le plus minimaliste possible. Typiquement, ce seront les services qu’il utilise qui seront mocké, et les tests serviront à déterminer si le contrôleur effectue bien les actions suivantes :

  • Vérification du modèle via ModelState.IsValid ;
  • Renvoie d’une erreur si le ModelState n’est pas valide ;
  • Récupération d’une entité persistée ;
  • Action sur l’entité persistée ;
  • Enregistrement de l’entité ;
  • Retour d’un objet de type IActionResult en fonction de l’action à effectuer.

Le test ci-dessous vérifie que l’index du contrôleur renvoi bien une liste d’un modèle attendue.

[Fact]
public void IndexReturnsAViewResultWithAList ()
{
     var mockRepo = new Mock<MonRepository>();
     mockRepo.Setup(r => r.List()).Returns(GetTest());
  
     var controller = new HomeController(mockRepo.Object);

     var result = Assert.IsType<ViewResult>(controller.Index());
     var model = Assert.IsAssignableFrom<IEnumerable<MonModel>>
                (result.ViewData.Model);

     Assert.Equal(2, model.Count());
}

Les tests unitaires et d’intégrations sont réellement un bon moyen de vérifier continuellement que le projet fonctionne correctement et que des bugs n’ont pas été intégrés au fur et à mesure du développement. Nous conseillons fortement d’inclure ces tests dès les premières lignes de code du projet, car plus le projet avance et plus il aura de tests à écrire.

Faites tourner ! Share on Facebook
Facebook
Tweet about this on Twitter
Twitter
Share on LinkedIn
Linkedin

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *