Benoît Courtine



    Navigation
     » Accueil
     » A propos
     » XML Feed

    Mise en place de tests unitaires sur Codingame

    13 Sep 2016 » java, qualite, codingame

    Présentation d’un puzzle Codingame

    L’interface

    Lorsqu’on se lance dans la résolution d’un puzzle sur Codingame, l’interface principale se décompose en quatre parties :

    • En haut à gauche, la description du puzzle (format attendu des entrées et des sorties, règles à implémenter), avec des illustrations et des exemples.

    • En bas à gauche, la sortie console.

    • En haut à droite, l’éditeur de code, dans lequel on développe sa solution (pré-configuré avec le code de lecture des entrées du puzzle).

    • En bas à droite, une liste de jeux de test préconfigurés, permettant de valider sa solution avant de la soumettre.

    Interface codingame
    Figure 1. Interface du puzzle codingame "Températures"
    J’aime beaucoup l’interface de Codingame. Elle est bien pensée, et met en avant l’importance des tests dans le développement, avec une approche TDD (les tests étant en place avant même le début du développement).

    Fonctionnement du puzzle

    Tous les puzzles (et ) Codingame fonctionnent de la même façon :

    • Les jeux de données d’entrée sont écrites sur l’entrée standard.

    • Le programme écrit la solution sur la sortie standard.

    • La sortie d’erreur est ignorée par les programmes de vérification du code, mais affichée dans la zone "Console" : elle est donc destinée au débogage.

    Utiliser IntelliJ IDEA

    Problématique

    Malgré ses efforts (autocomplétion rudimentaire, etc.), l’interface de développement Codingame est assez vite limitée. Pour résoudre les puzzles un peu complexes, je préfère donc développer dans IntelliJ IDEA.

    Codingame fournit un plugin de synchronisation pour Chrome, mais le fonctionnement de celui-ci ne me convenait pas. Il m’oblige en particulier à utiliser le même fichier pour tous les puzzles (une classe "Solution" dans le package par défaut).

    Je suis donc parti sur un simple projet Maven, donc je copie-colle le code vers Codingame une fois le développement terminé.

    Mise en place du projet avec Maven

    Je suis parti sur la solution suivante :

    • Utilisation de Git pour versionner mon avancement.

    • Utilisation de Maven pour gérer les dépendances de test (les librairies tierces étant interdites dans le code principal).

    Fichier pom.xml du projet
    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>org.courtine</groupId>
      <artifactId>codingame</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>jar</packaging>
    
      <name>Solutions des puzzles Codingame</name>
    
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      </properties>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
    
      <dependencies>
        <!-- TestNG peut être remplacé par JUnit, en fonction des préférences. -->
        <dependency>
          <groupId>org.testng</groupId>
          <artifactId>testng</artifactId>
          <version>6.9.10</version>
          <scope>test</scope>
        </dependency>
        <!--  Dépendances facultatives, mais simplifiant l'écriture des tests. -->
        <dependency>
          <groupId>org.assertj</groupId>
          <artifactId>assertj-core</artifactId>
          <version>3.3.0</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>com.google.guava</groupId>
          <artifactId>guava</artifactId>
          <version>19.0</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>

    Les tests

    Lancement manuel

    Pour lancer les tests, plusieurs solutions :

    • Copier-coller le code vers Codingame et les lancer depuis l’interface du site.

    • Lancer la méthode main() du puzzle, et copier-coller le jeu de test de Codingame vers la console IntelliJ.

    Ces deux solutions "manuelles" sont assez fastidieuses, et obligent à faire des aller-retour entre l’IDE et le site. Afin de pouvoir les tests directement depuis IntelliJ, nous allons recopier les jeux de test fournis sous la forme de tests unitaires.

    Rendre le code testable

    Code initial généré par Codingame
    import java.util.*;
    import java.io.*;
    import java.math.*;
    
    class Solution {
    
      public static void main(String args[]) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt(); // the number of temperatures to analyse
        in.nextLine();
        String temps = in.nextLine(); // the n temperatures expressed as integers ranging from -273 to 5526
    
        System.out.println("result");
      }
    }

    Ce code initial utilisant l’entrée et la sortie standard, il est assez difficilement testable en l’état. Nous allons donc le décomposer, en séparant la partie I/O, le parsing de l’énoncé, et le cœur du calcul.

    Code décomposé
    import java.util.*;
    
    class Solution {
    
      public static void main(String args[]) {
        List<Integer> temps = parseInput(System.in);
        int result = nearestFromZero(temps);
        System.out.println(result);
      }
    
      public static List<Integer> parseInput(InputStream is) {
        Scanner in = new Scanner(is);
        int n = in.nextInt(); // the number of temperatures to analyse
        in.nextLine();
        List<Integer> temps = new ArrayList<Integer>();
        for (int i = 0; i < n; i++) {
          temps.add(in.nextInt());
        }
        return temps;
      }
    
      public static int nearestFromZero(List<Integer> temps) {
        // TODO
        return Integer.MAX_VALUE;
      }
    }

    Avec ce code décomposé, on peut donc vérifier unitairement que la méthode de parsing fonctionne, mais surtout vérifier le comportement du code "métier" répondant aux différents cas de test du puzzle.

    Recopie des tests issus de Codingame

    La classe SolutionTest
    import com.google.common.collect.Lists;
    import org.testng.annotations.Test;
    
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.nio.charset.StandardCharsets;
    import java.util.List;
    
    import static org.assertj.core.api.Assertions.assertThat;
    
    public class SolutionTest {
    
      /** Vérification du comportement de la méthode de parsing du flux d'entrée. */
      @Test
      public void test_technique_de_la_methode_de_parsing() {
        String input = "5\n1 2 3 4 5";
        InputStream is = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
        List<Integer> temps = Solution.parseInput(is);
    
        assertThat(temps).containsExactly(1, 2, 3, 4, 5);
      }
    
      /**
       * Dans le cas où le jeu de données est volumineux, on peut préférer le stocker dans un
       * fichier dédié plutôt que dans une chaîne de caractères. On peut alors écrire le test
       * de cette façon en plaçant le fichier dans le répertoire "src/test/resources".
       */
      @Test
      public void test_de_parsing_en_utilisant_un_fichier_en_entree() throws IOException {
        try (InputStream is = getClass().getResourceAsStream("/jeu_de_test.txt")) {
          List<Integer> temps = Solution.parseInput(is);
          assertThat(temps).containsExactly(1, 2, 3, 4, 5);
        }
      }
    
      @Test
      public void test_codingame_donnees_simples() {
        List<Integer> temps = Lists.newArrayList(1, -2, -8, 4, 5);
        assertThat(Solution.nearestFromZero(temps)).isEqualTo(1);
      }
    
      /**
       * Si le jeu de données d'entrée est complexe, on peut enchaîner le parsing depuis le
       * fichier d'entrée (le fichier de données et le code de parsing étant directement
       * fournis par Codingame) avec le test de la méthode de calcul principale.
       */
      @Test
      public void test_codingame_donnees_simples_dans_un_fichier() {
        try (InputStream is = getClass().getResourceAsStream("/donnees_simples.txt")) {
          List<Integer> temps = Solution.parseInput(is);
          assertThat(Solution.nearestFromZero(temps)).isEqualTo(1);
        }
      }
    
      // Recopier l'ensemble des tests en provenance de Codingame… en rajoutant des tests
      // supplémentaires en cas de besoin.
    }
    Les classes d’exemple ci-dessus n’ont pas de package. En réalité, je développe chaque solution de puzzle dans un package dédié (org.courtine.codingame.easy.temperatures par exemple, dans le cas qui nous intéresse).

    Conclusion

    Ce projet mis en place, nous pouvons exécuter les tests de validation depuis notre IDE. Pour ce puzzle d’exemple "Températures" qui se résout en quelques lignes de code, c’est un peu extrême, mais pour les puzzles plus complexes, décomposer le code en de petites méthodes que l’on peut tester unitairement s’avère indispensable.