Générateurs de tests


Un générateur de tests est un logiciel propre à un parcours, qui génère automatiquement les tests d'un exercice d'entraînement. Pour ce faire, il convertit les cas de test JSON de l'exercice en tests dans le langage du parcours.

Avantages

Voici quelques avantages d'un générateur de tests :

  1. On peut ajouter des exercices plus rapidement
  2. Il automatise les parties « ennuyeuses » de l'ajout d'un exercice
  3. Il est facile de synchroniser les tests avec les dernières données canoniques

Cas d'usage

En général, on lance un générateur de tests pour :

  1. Générer les tests d'un exercice nouveau
  2. Mettre à jour les tests d'un exercice existant

Générer les tests d'un nouvel exercice

Ajouter un générateur de tests pour un nouvel exercice permet de générer son ou ses fichiers de tests. À condition que le générateur de tests lui-même soit déjà implémenté, générer les tests du nouvel exercice demandera (bien) moins de travail que de les écrire à partir de zéro.

Mettre à jour les tests d'un exercice existant

Une fois qu'un exercice dispose d'un générateur de tests, tu peux le relancer pour mettre à jour ou synchroniser l'exercice avec ses dernières données canoniques. Nous te recommandons de le faire régulièrement, pour vérifier s'il y a des cas de test problématiques à mettre à jour ou de nouveaux tests que tu pourrais vouloir inclure.

Point de départ

Il y a deux points de départ possibles lorsqu'on implémente un générateur de tests pour un exercice :

  1. L'exercice est nouveau et n'a donc aucun test
  2. L'exercice existe déjà et possède donc des tests existants
Caution

S'il existe déjà des tests, implémente le générateur de tests de sorte que les tests qu'il génère ne cassent pas les solutions existantes.

Conception

D'une manière générale, les fichiers de tests sont générés de deux façons :

  • Code : les fichiers de tests sont (en grande partie) générés par du code
  • Gabarits : les fichiers de tests sont (en grande partie) générés à l'aide de gabarits

Nous avons constaté que l'approche fondée sur le code produit un code de générateur de tests assez complexe, tandis que l'approche fondée sur les gabarits est plus simple.

Voici la marche à suivre recommandée :

  1. Lire les données canoniques de l'exercice
  2. Exclure les cas de test marqués comme include = false dans le fichier tests.toml de l'exercice
  3. Convertir les données canoniques de l'exercice dans un format utilisable dans un gabarit
  4. Passer les données canoniques de l'exercice à un gabarit propre à l'exercice

Le principal avantage de cette organisation est que chaque exercice dispose de son propre gabarit, ce qui :

  • rend évidente la manière dont les fichiers de tests sont générés
  • les rend plus faciles à déboguer
  • permet de les modifier sans risquer de casser un autre exercice
Caution

Lors de la conception du générateur de tests, essaie de :

  • minimiser le prétraitement des données canoniques à l'intérieur du générateur de tests
  • réduire le couplage entre les gabarits

Implémentation

Le générateur de tests est généralement (en grande partie) écrit dans le langage du parcours.

Caution

Tu es libre d'utiliser d'autres langages, mais chaque langage supplémentaire rendra la maintenance du parcours ou les contributions plus difficiles. Par conséquent, nous recommandons d'utiliser le langage du parcours lorsque c'est possible, car cela facilite la maintenance et les contributions.

Mise en forme

Si ton parcours dispose d'outils pour mettre en forme le code, envisage de lancer cette mise en forme comme étape de post-traitement après le rendu de ton gabarit.

Données canoniques

La donnée centrale avec laquelle travaille le générateur de tests est le fichier canonical-data.json d'un exercice. Ce fichier est défini dans le dépôt exercism/problem-specifications, qui définit des métadonnées partagées pour de nombreux exercices d'Exercism.

Caution

Tous les exercices n'ont pas de fichier canonical-data.json ! S'ils n'en ont pas, tu devras créer les tests manuellement, car il n'y a aucune donnée avec laquelle le générateur de tests puisse travailler.

Structure

Les données canoniques sont définies dans un objet JSON. Cet objet contient un champ "cases" qui contient les cas de test. Ces cas de test correspondent (normalement) un pour un aux tests de ton parcours.

Chaque cas de test possède plusieurs propriétés, dont les plus importantes sont la description, la propriété, la ou les valeurs d'entrée et la valeur attendue. Voici un exemple (partiel) du fichier canonical-data.json de l'exercice leap :

{
  "exercise": "leap",
  "cases": [
    {
      "uuid": "6466b30d-519c-438e-935d-388224ab5223",
      "description": "year not divisible by 4 in common year",
      "property": "leapYear",
      "input": {
        "year": 2015
      },
      "expected": false
    },
    {
      "uuid": "4fe9b84c-8e65-489e-970b-856d60b8b78e",
      "description": "year divisible by 4, not divisible by 100 in leap year",
      "property": "leapYear",
      "input": {
        "year": 1996
      },
      "expected": true
    }
  ]
}

La principale responsabilité du générateur de tests est de transformer ces données JSON en tests propres au parcours. Voici comment les données JSON ci-dessus peuvent se traduire en code de test Nim :

import unittest
import leap

suite "Leap":
  test "year not divisible by 4 in common year":
    check isLeapYear(2015) == false

  test "year divisible by 4, not divisible by 100 in leap year":
    check isLeapYear(1996) == true

La structure du fichier canonical-data.json est bien documentée et il dispose aussi d'une définition de schéma JSON.

Imbrication

Certains exercices utilisent l'imbrication dans leurs données canoniques. Cela signifie que chaque élément d'un tableau cases peut être soit :

  1. Un cas de test ordinaire (sans cas de test enfant)
  2. Un groupe de cas de test (un ou plusieurs cas de test enfants)
Note

Tu peux identifier le type d'un élément en vérifiant la présence de champs exclusifs à un type d'élément. La meilleure façon de procéder est probablement d'utiliser la clé "cases", qui n'est présente que dans les groupes de cas de test.

Voici un exemple de cas de test imbriqués :

{
  "cases": [
    {
      "uuid": "e9c93a78-c536-4750-a336-94583d23fafa",
      "description": "data is retained",
      "property": "data",
      "input": {
        "treeData": ["4"]
      },
      "expected": {
        "data": "4",
        "left": null,
        "right": null
      }
    },
    {
      "description": "insert data at proper node",
      "cases": [
        {
          "uuid": "7a95c9e8-69f6-476a-b0c4-4170cb3f7c91",
          "description": "smaller number at left node",
          "property": "data",
          "input": {
            "treeData": ["4", "2"]
          },
          "expected": {
            "data": "4",
            "left": {
              "data": "2",
              "left": null,
              "right": null
            },
            "right": null
          }
        }
      ]
    }
  ]
}
Caution

Si ton parcours ne prend pas en charge le regroupement des tests, tu devras :

  • parcourir ou aplatir la hiérarchie cases pour ne conserver que les cas de test les plus profonds (les feuilles)
  • combiner la description d'un cas de test avec celle ou celles de ses parents pour créer un nom de test unique

Valeurs d'entrée et attendues

Le contenu des clés input et expected d'un cas de test varie énormément. Dans la plupart des cas, il s'agit de valeurs scalaires (comme des nombres, des booléens ou des strings) ou d'objets simples. Il t'arrivera cependant de rencontrer des valeurs plus complexes qui demanderont sans doute un peu de prétraitement, comme des lambdas en pseudo-code, des tableaux d'opérations à effectuer sur le code des apprenants, entre autres.

Scénarios

Les cas de test ont un champ facultatif scenarios. Ce champ permet au générateur de tests de traiter certains cas de test de manière particulière. Le cas d'usage le plus courant consiste à ignorer certains types de tests, par exemple les tests avec le scénario "unicode", car le langage de ton parcours peut ne pas prendre en charge Unicode.

La liste complète des scénarios se trouve ici.

Lire les fichiers canonical-data.json

Il existe plusieurs options pour lire les fichiers canonical-data.json :

  1. Les récupérer directement depuis le dépôt problem-specifications (par exemple https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json).
  2. Ajouter le dépôt problem-specifications comme sous-module Git au dépôt du parcours.
  3. Les lire depuis le cache de configlet. L'emplacement dépend du système de l'utilisateur, mais tu peux utiliser configlet info -o -v d | head -1 | cut -d " " -f 5 pour obtenir cet emplacement par programmation.

Cas de test propres au parcours

Si ton parcours souhaite ajouter des cas de test supplémentaires propres au parcours (absents des données canoniques), une option consiste à créer un fichier additional-test-cases.json, que le générateur de tests pourra ensuite fusionner avec le fichier canonical-data.json avant de le passer au gabarit pour le rendu.

Gabarits

Le moteur de gabarits à utiliser sera probablement propre au parcours. Idéalement, tu voudras que tes gabarits soient aussi simples que possible, alors ne te soucie pas de la duplication de code et de ce genre de choses.

Les gabarits eux-mêmes reçoivent leurs données du générateur de tests, qu'ils parcourent pour effectuer le rendu.

Note

Pour aider à garder les gabarits simples, il peut être utile d'effectuer un peu de prétraitement du côté du générateur de tests, ou bien de définir quelques « filtres » ou tout autre mécanisme d'extension que tes gabarits autorisent.

Utiliser configlet

configlet est le principal outil de maintenance d'un parcours et peut servir à :

  • créer les fichiers d'un nouvel exercice : exécute bin/configlet create --practice-exercise <slug>
  • synchroniser le fichier tests.toml d'un exercice existant : exécute bin/configlet sync --tests --update --exercise <slug>
  • récupérer sur le disque les données canoniques de l'exercice (c'est un effet secondaire de l'une ou l'autre des commandes ci-dessus)

Cela fait de configlet un excellent outil à combiner avec le générateur de tests pour obtenir des workflows vraiment puissants.

Interface en ligne de commande

Tu voudras rendre l'utilisation du générateur de tests à la fois simple et puissante. Pour cela, nous recommandons de créer un ou plusieurs fichiers de script.

Note

Tu es libre de choisir le format de fichier de script qui convient le mieux à ton parcours. Les scripts shell et les scripts PowerShell sont des options courantes qui fonctionnent bien toutes les deux.

Voici un exemple de script shell qui combine configlet et un générateur de tests pour créer rapidement la structure d'un nouvel exercice :

bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>

Construire à partir de zéro

Avant de commencer à construire un générateur de tests, nous te suggérons de regarder quelques générateurs de tests existants pour te faire une idée de la façon dont d'autres parcours les ont implémentés :

Si tu as des questions, le forum est le meilleur endroit pour les poser. Les discussions du forum sur les générateurs de tests Rust et JavaScript pourront aussi t'être utiles.

Produit minimum viable

Nous recommandons de construire le générateur de tests de façon incrémentale, en commençant par un produit minimum viable. Une version minimale se contenterait de lire le fichier canonical-data.json d'un exercice et de passer ces données au gabarit.

Commence par te concentrer sur un seul exercice, de préférence un exercice simple comme leap. Ce n'est qu'une fois que cela fonctionne que tu devrais ajouter progressivement d'autres exercices.

Et essaie de garder le générateur de tests aussi simple que possible.

Note

Idéalement, un contributeur pourrait simplement coller ou modifier un gabarit existant sans avoir à comprendre le fonctionnement interne du générateur de tests.

Utiliser ou contribuer

La façon d'utiliser un générateur de tests ou d'y contribuer est propre à chaque parcours. Cherche les instructions dans le README.md du parcours, son CONTRIBUTING.md ou le dossier contenant le code du générateur de tests.