Appendix A. Automatiser vos tests unitaires et d’intégration

A.1. Automatiser vos tests avec Maven

Maven est un outil de build open source populaire dans le monde Java, qui fait usage de pratiques telles que les déclarations de dépendances, les répertoires et cycles de vie de build standards, et la convention préférée à la déclaration pour encourager des scripts de build propres, maintenables et de bonne qualité. L’automatisation des tests est fortement prise en charge par Maven. Les projets Maven utilisent une structure de dossiers standard : les tests unitaires seront automatiquement recherchés dans un dossier nommé (par défaut) src/test/java. Il y a quelques petits réglages supplémentaires : en ajoutant juste une dépendance à un (ou plusieurs) framework(s) de test utilisé(s) par vos tests, Maven détectera et exécutera automatiquement les tests JUnit, TestNG, ou même Plain Old Java Objects (POJO) contenus dans cette structure de dossiers.

Avec Maven, vous lancez vos tests unitaires en invocant la phase test du cycle de vie, comme présenté ici :

$ mvn test
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building Tweeter domain model
[INFO]    task-segment: [test]
[INFO] ------------------------------------------------------------------------
...
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.wakaleo.training.tweeter.domain.TagTest
Tests run: 13, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.093 sec
Running com.wakaleo.training.tweeter.domain.TweeterTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.021 sec
Running com.wakaleo.training.tweeter.domain.TweeterUserTest
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.055 sec
Running com.wakaleo.training.tweeter.domain.TweetFeeRangeTest
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.051 sec
Running com.wakaleo.training.tweeter.domain.HamcrestTest
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.023 sec

Results :

Tests run: 38, Failures: 0, Errors: 0, Skipped: 0

En plus d’exécuter vos tests, et de mettre le build en échec dès qu’un test échoue, Maven va produire un ensemble de rapports de tests (encore, par défaut) dans le dossier target/surefire-reports, dans les formats XML et texte. Pour nos objectifs d’intégration continue, ce sont les fichiers XML qui nous intéressent puisque Jenkins est en mesure de comprendre et d’analyser ces fichiers pour ses rapports d’intégration :

$ ls target/surefire-reports/*.xml
target/surefire-reports/TEST-com.wakaleo.training.tweeter.domain.HamcrestTest.xml
target/surefire-reports/TEST-com.wakaleo.training.tweeter.domain.TagTest.xml
target/surefire-reports/TEST-com.wakaleo.training.tweeter.domain.TweetFeeRangeTest.xm
target/surefire-reports/TEST-com.wakaleo.training.tweeter.domain.TweeterTest.xml
target/surefire-reports/TEST-com.wakaleo.training.tweeter.domain.TweeterUserTest.xml

Maven définit deux phases de tests distinctes : les tests unitaires et les tests d’intégration. Les tests unitaires doivent être rapides et léger, en fournissant une quantité importante de retours de test en un minimum de temps. Les tests d’intégration sont plus lents et lourds, et requièrent souvent que l’application soit construite et déployée sur un serveur (potentiellement embarqué) pour supporter des tests plus complets. Ces deux types de tests sont importants, et pour un environnement d’Intégration Continue bien conçu, il est nécessaire de bien les distinguer. Le build doit assurer que tous les tests unitaires sont lancés en premier - si un test unitaire échoue, les développeurs devraient en être notifiés très rapidement. Le lancement des tests d’intégration, lents et plus lourds, ne vaut le coup que si tous les tests unitaires passent.

Avec Maven, les tests d’intégration sont exécutés pendant la phase integration-test du cycle de vie, que vous pouvez invoquer en lançant mvn integration-test ou (plus simplement) mvn verify. Pendant cette phase, il est facile de configurer Maven pour démarrer votre application web sur un serveur Jetty embarqué, ou pour packager et déployer votre application sur un serveur de test, par exemple. Vos tests d’intégration peuvent ensuite être exécutés sur l’application en marche. Cependant, la partie délicate, est de dire à Maven comment distinguer les tests unitaires des tests d’intégration, afin qu’ils ne soient exécutés que si une version fonctionnelle de l’application est disponible.

Il y a plusieurs manières d’y parvenir, mais au moment de l’écriture il n’existe pas d’approche standard officielle utilisée à travers tous les projets Maven. Une stratégie simple est d’utiliser les conventions de nommage : tous les tests d’intégration peuvent se terminer par “IntegrationTest”, ou être placés dans un package particulier. La classe suivante utilise une convention de la sorte :

public class AccountIntegrationTest {
  
  @Test
  public void cashWithdrawalShouldDeductSumFromBalance() throws Exception {
    Account account = new Account();
    account.makeDeposit(100);
    account.makeCashWithdraw(60);
    assertThat(account.getBalance(), is(40));
  }
}

Avec Maven, les tests sont configurés via le plugin maven-surefire-plugin. Pour assurer que Maven lance ces tests seulement pendant la phase integration-test, vous pouvez configurer ce plugin comme présenté ici :

<project>
  ...
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <skip>true</skip>1
        </configuration>
        <executions>
          <execution>2
            <id>unit-tests</id>
            <phase>test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <skip>false</skip>
              <excludes>
                <exclude>**/*IntegrationTest.java</exclude>
              </excludes>
            </configuration>
          </execution>
          <execution>3
            <id>integration-tests</id>
            <phase>integration-test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <skip>false</skip>
              <includes>
                <include>**/*IntegrationTest.java</include>
              </includes>
            </configuration>
          </execution>
        </executions>
      </plugin>
      ...
1

Saute tous les tests par défaut — ceci désactive la configuration par défaut des tests pour Maven.

2

Pendant la phase des tests unitaires, lance les tests en excluant les tests d’intégration.

3

Pendant la phase des tests d’intégration, lance les tests mais en incluant seulement les tests d’intégration.

Ceci assure que les tests d’intégration seront ignorés pendant la phase des tests unitaires, et exécutés seulement pendant la phase des tests d’intégration.

Si vous ne voulez pas ajouter de contrainte non souhaitée sur les noms de vos classes de test, vous pouvez utiliser les noms de package plutôt. Dans le projet illustré dans Figure A.1, “Un projet contenant des classes de tests nommées librement”, tous les tests fonctionnels ont été placés dans un package nommé webtests. Il n’y a aucune contrainte sur les noms des tests, mais nous utilisons des Page Objects pour modéliser l’interface de notre application, donc nous nous assurons aussi qu’aucune classe du package pages (à l’intérieur du package webtests) ne soit considéré comme un test.

Un projet contenant des classes de tests nommées librement

Figure A.1. Un projet contenant des classes de tests nommées librement


Avec Maven, nous pouvons faire cela avec la configuration suivante :

      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <skip>true</skip>
        </configuration>
        <executions>
          <execution>
            <id>unit-tests</id>
            <phase>test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <skip>false</skip>
              <excludes>
                <exclude>**/webtests/*.java</exclude>
              </excludes>
            </configuration>
          </execution>
          <execution>
            <id>integration-tests</id>
            <phase>integration-test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <skip>false</skip>
              <includes>
                <include>**/webtests/*.java</include>
              </includes>
              <excludes>
                <exclude>**/pages/*.java</exclude>
              </excludes>
            </configuration>
          </execution>
        </executions>
      </plugin>

TestNG a actuellement un support plus flexible des groupes de test que JUnit. Si vous utilisez TestNG, vous pouvez identifier vos tests d’intégration en utilisant les TestNG Groups. Avec TestNG, les classes et les méthodes de test peuvent être étiquetées en utilisant l’attribut groups de l’annotation @Test, comme présenté ici :

@Test(groups = { "integration-test" })
public void cashWithdrawalShouldDeductSumFromBalance() throws Exception {
    Account account = new Account();
    account.makeDeposit(100);
    account.makeCashWithdraw(60);
    assertThat(account.getBalance(), is(40));
}

En utilisant Maven, vous pouvez vous assurer que ces tests sont seulement lancés pendant la phase des tests d’intégration avec la configuration suivante :

<project>
  ...
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <skip>true</skip>
        </configuration>
        <executions>
          <execution>
            <id>unit-tests</id>
            <phase>test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <skip>false</skip>
              <excludedGroups>integration-tests</excludedGroups>1
            </configuration>
          </execution>
          <execution>
            <id>integration-tests</id>
            <phase>integration-test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <skip>false</skip>
              <groups>integration-tests</groups>2
            </configuration>
          </execution>
        </executions>
      </plugin>
      ...
1

Ne lance pas le groupe integration-tests pendant la phase test.

2

Lance seulement les tests du groupe integration-tests pendant la phase integration-test.

Il est souvent intéressant de lancer les tests en parallèle dès que possible, puisque cela peut accélérer vos tests de façon significative (voir Section 6.9, “A l'aide ! Mes tests sont trop lents !”). Les tests en parallèle sont particulièrement efficaces avec des tests lents qui utilisent beaucoup d’accès E/S, disque ou réseau (comme les tests web), ce qui est pratique, puisque ce sont précisément les types de tests que nous voulons généralement accélérer.

TestNG propose un bon support des tests en parallèle. Par exemple, avec TestNG, vous pouvez configurer vos méthodes de tests pour qu’elles se lancent en parallèle sur dix threads concurrents comme ceci :

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.5</version>
        <configuration>
          <parallel>methods</parallel>
          <threadCount>10</threadCount>
        </configuration>
      </plugin>

Depuis JUnit 4.7, vous pouvez aussi lancer vos tests JUnit en parallèle en utilisant une configuration similaire. En fait, la configuration présentée ci-dessus fonctionnera pour JUnit 4.7 et suivant.

Vous pouvez aussi régler le paramètre de configuration <parallel> à la valeur classes au lieu de methods, ce qui tentera de lancer les classes de test en parallèle, plutôt que pour chaque méthode. Cela peut être plus ou moins rapide, en fonction du nombre de classes de test que vous avez, mais peut être plus sûr pour certains cas de test non conçus avec la concurrence à l’esprit.

Les résultats peuvent varier, donc vous feriez bien d’expérimenter les nombres pour obtenir les meilleurs résultats.