Aller au contenu

Atelier 09

Dans cette session de laboratoire, vous pratiquerez deux aspects avancés du développement logiciel : les tests de mutation et les mock tests. Pour les exercices de cette dernière session de laboratoire, vous travaillerez avec le projet PrimeNumbers et le project MockServerExercise disponibles sur GitLab.

Tests de mutation

  • Les tests de mutation sont un test de stress automatisé pour les tests unitaires et d'intégration existants.
  • L'idée principale est d'injecter des erreurs dans un programme supposé correct et de vérifier si au moins un test échoue pour chaque erreur injectée.
  • Chaque variante du logiciel original injectée d'une erreur est appelée un mutant.
  • Si aucun test n'est capable de détecter la modification artificielle du programme, les tests associés sont considérés comme contenant des zombies, c'est-à-dire des tests inutiles ou insuffisants.

Pitest

  • Le plugin pitest est une extension pratique au processus de construction Maven.
  • L'installation est aussi simple que d'ajouter une dépendance et un plugin suivant dans la configuration pom.xml :

    • Dépendance:
      <dependency>
          <groupId>org.pitest</groupId>
          <artifactId>pitest-junit5-plugin</artifactId>
          <version>1.2.3</version>
          <scope>test</scope>
      </dependency>
      
    • Plugin:
      <!-- Mutation test report-->
      <!-- HTML report available at:  target/pit-reports/index.html  -->
      <plugin>
          <groupId>org.pitest</groupId>
          <artifactId>pitest-maven</artifactId>
          <version>1.17.1</version>
          <configuration>
              <mutationThreshold>65</mutationThreshold>
          </configuration>
          <!-- Mutation tests are expensive (slow), and there's no point in executing them if common tests fail. Therefore we execute them later, in the verify phase.-->
          <executions>
              <execution>
                  <id>mutation-tests</id>
                  <goals>
                      <goal>mutationCoverage</goal>
                  </goals>
                  <phase>verify</phase>
              </execution>
          </executions>
      </plugin>
      
  • Ensuite, la création d'un rapport de test de mutation est déclenchée par la phase test, c'est-à-dire mvn clean test

  • Le rapport est disponible dans target/pit-reports/index.html et peut être consulté avec n'importe quel navigateur web.

À vous de jouer

Exemple de configuration du code :

  • Clonez le projet de vérification des nombres premiers (sous-répertoire l11/PrimeChecker) :
git clone git@gitlab.info.uqam.ca:inf2050/labs.git
  • Ouvrez le projet dans IntelliJ
  • Exécutez les tests fournis, en utilisant mvn clean test

Tous les tests réussiront, mais cela signifie-t-il que vos tests sont bons ? Voyons cela :

  • Activez le plugin de test de mutation.
  • Exécutez à nouveau les tests et générez un rapport de test de mutation : mvn clean test
  • Consultez le rapport de test de mutation dans votre navigateur.

Le rapport vous indiquera une mutation de négation qui peut être ajoutée à votre code sans que les tests ne le remarquent.

  • Identifiez quelle mutation il s'agit, c'est-à-dire quelle ligne, quel changement.
  • Vérifiez la mutation, en modifiant effectivement le code de la même manière (votre code comptera désormais les nombres non-premiers au lieu des nombres premiers).
  • Exécutez à nouveau les tests.
Que devrait-il se passer ?

Les résultats des tests devraient encore être tous réussis. Les résultats du test de mutation indiquent que vos tests ne sont pas capables d'identifier la modification de votre code source.

Enfin, il est temps de corriger le problème et d'améliorer vos tests !

  • Revoyez vos tests. Identifiez le problème avec vos tests existants.
  • Modifiez les tests existants ou ajoutez-en de nouveaux.
  • Exécutez à nouveau les tests de mutation et vérifiez que la couverture des mutations est désormais de 100%.

Tests d'intégration

  • Lors du dernier cours, vous avez appris que le cycle de vie par défaut de Maven définit deux phases de tests différentes :
    • test pour l'exécution des tests unitaires.
    • verify pour l'exécution des tests d'intégration.

Tests unitaires vs Tests d'intégration

Les tests unitaires vérifient le comportement des classes ou des méthodes individuelles. Les tests d'intégration vérifient le comportement de l'interaction entre les composants, s'assurant que l'application dans son ensemble remplit correctement son objectif.

Mad Max

Une façon de tester l'intégration des implémentations de jeu, par exemple le projet XoxInternals, est d'écrire des joueurs robots (ou, si vous voulez utiliser un langage à la mode, des "IA").

  • Alors que les tests unitaires vérifient les fonctions individuelles du jeu, les joueurs robots tentent de jouer au jeu et vérifient ainsi s'il fonctionne de manière fiable.
  • La façon la plus simple de tester cela est avec le joueur robot "Mad Max".
    • Mad Max utilise un générateur aléatoire pour choisir une action au hasard, chaque fois que c'est à son tour de jouer.
    • Deux instances concurrentes de Mad Max doivent finir par manœuvrer le jeu jusqu'à un état de fin de partie.
    • Le test d'intégration est réussi si la partie se termine sans erreur dans les 5 secondes du processeur.
  • Un générateur de nombres aléatoires avec une graine fixée peut être utilisé pour garantir des chemins d'exécution déterministes des joueurs robots.
  • Pour des tests avancés, 1000 instances de jeu peuvent être testées, chacune avec une graine individuelle (par exemple, une valeur de graine incrémentée comme 1, 2, 3, ...).

Les tests d'intégration sont également des tests, et doivent donc être placés dans la structure de dossiers src/test/java.

  • Pour distinguer les tests unitaires standards (qui doivent être exécutés lors de la phase test) des tests d'intégration (qui doivent être exécutés lors de la phase verify), une syntaxe de nom de fichier spéciale peut être utilisée.
  • Les tests d'intégration doivent se terminer par ...IT.java.
  • Une configuration supplémentaire dans le fichier pom.xml pour le plugin de test permet de séparer l'exécution en fonction du nom du fichier :
    <project>
      ...
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.2</version>
            <configuration>
              <excludes>
                <exclude>**/*IT.java</exclude>
              </excludes>
            </configuration>
            <executions>
              <execution>
                <id>integration-test</id>
                <goals>
                  <goal>test</goal>
                </goals>
                <phase>integration-test</phase>
                <configuration>
                  <excludes>
                    <exclude>none</exclude>
                  </excludes>
                  <includes>
                    <include>**/*IT.java</include>
                  </includes>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </project>
    

À vous de jouer

  1. Ajoutez le plugin maven-surefire-plugin ci-dessus à votre projet XoxFrontend.
  2. Créez une nouvelle classe de test MadMaxIT.java.
  3. Dans MadMaxIT, créez une fonction décorée avec @Test qui exécute 1000 parties différentes de Xox avec deux joueurs robots MadMax en compétition.
  4. Vérifiez que les tests d'intégration passent à travers les 1000 instances de jeu sans planter.

  5. mvn clean test ne doit pas exécuter les tests d'intégration.

  6. mvn clean verify doit exécuter les tests d'intégration.

Vous pouvez utiliser les morceaux de code ci-dessous pour implémenter la fonctionnalité demandée :

  /**
 * Provides a random number between 0 and the provided value (exclusive).
 *
 * @param upperBoundExcluded as the maximum value that is guaranteed not to be reached.
 */
public int pickRandomAction(int upperBoundExcluded) {
  return Math.abs(randomNumberGenerator.nextInt()) % upperBoundExcluded;
}

/**
 * Initialize seeded random number generator. Each game test-run of two competing MadMax players
 * should use a different seed.
 */
public void initializeRandomNumberGenerator(int seed) {
  // For seed 42, will always generate:
  // -1170105035 234785527 -1360544799 205897768 ...
  randomNumberGenerator = new Random(seed);
}

/**
 * The integration test creates 1000 test games and verifies that each of them is successfully
 * finished by two competing MadMax robot players. Every game uses a different random generator
 * seed.
 */
@Test
public void madMax() {
  // Run 1000 random games. All must conclude in valid game ending
  for (int i = 0; i < 1000; i++) {
    runSeededRandomGame(i);
  }
}

private void runSeededRandomGame(int seed) {
  // TODO: implement missing functionality to finish seeded game
  long gameId = XoxGameInitiator.createNewGame(playerNames[0], playerNames[1]);
  while (!gameOver) {
    // select a random move for the current player
    XoxClaimFieldAction[] allPossibleActions =
        gameManager.getActions(gameId, playerNames[currentPlayer]);
    // pick and play the random move
    gameManager.performAction(gameId, playerNames[currentPlayer], randomAction);
    // advance current player and update game over status
    gameOver = gameManager.getRanking(gameId).isGameOver();
  }
}

Moquer

  • Dans le dernier cours, vous avez vu un exemple de dépendance d'un SUT (base de données), qui devait être remplacée par un Mock pour faciliter les tests.
  • Dans ce qui suit, vous allez appliquer les mêmes concepts à une application différente, le TagCounter.
  • Le cas d'utilisation est identique : nous avons un SUT (classe à tester), qui fait une seule chose : compter les balises HTML.

Exemple

Cet échantillon HTML :

<html>
<h1> Hello, World! </h1>
</html>

possède 4 tags:

  • <html>
  • <h1>
  • </h1>
  • </html>

Dependency illustration

Malheureusement, nous ne pouvons pas tester notre TagCounter de manière isolée. Pour l'instancier, nous avons également besoin d'un objet ServerFileDownloader,
qui fournit le code HTML en le téléchargeant depuis un vrai serveur : Wikipédia

---
title: SUT needs a ServerFileDownloader object
---
classDiagram
    TagCounter *--> ServerFileDownloader: has a

    class ServerFileDownloader {
        <<Class>>
        +getWebpageContent() void
    }

    class TagCounter {
        <<Class>>
        +TagCounter(ServerFileDownloader) TagCounter
        +countTags() int
    }

À vous de jouer

  • Téléchargez le projet ServerMockExercise préparé.
  • Exécutez l'application et familiarisez-vous avec le code existant.
  • Modifiez uniquement le code de test pour permettre le test avec mock
    • Ajoutez Mockito comme dépendance de test
      dans le fichier pom.xml.
    • Ouvrez la classe de test préparée : TagCounterTest et utilisez les annotations vues en cours pour remplacer la
      dépendance du serveur par une dépendance mock.
    • Ajoutez une instruction when-thenReturns pour préparer une classe de test. Retournez une page web minimale,
      par exemple celle listée ci-dessus.
    • Ajoutez une instruction assert à votre test pour vérifier que le TagCounter fonctionne correctement.
    • Utilisez un Captor pour vérifier que l'objet mock ServerFileDownloader est correctement invoqué.