Generatori di test


Un generatore di test è un software specifico per un track che genera automaticamente i test di un esercizio di pratica. Lo fa convertendo i casi di test JSON dell'esercizio in test nel linguaggio del track.

Vantaggi

Alcuni vantaggi dell'avere un generatore di test:

  1. Gli esercizi si possono aggiungere più rapidamente
  2. Automatizza le parti «noiose» dell'aggiungere un esercizio
  3. Rende semplice sincronizzare i test con i dati canonici più recenti

Casi d'uso

In generale, si esegue un generatore di test per:

  1. Generare i test per un esercizio nuovo
  2. Aggiornare i test di un esercizio esistente

Generare i test per un nuovo esercizio

Aggiungere un generatore di test per un nuovo esercizio permette di generarne i file di test. A patto che il generatore di test sia già stato implementato, generare i test per il nuovo esercizio richiederà (molto) meno lavoro che scriverli da zero.

Aggiornare i test di un esercizio esistente

Una volta che un esercizio ha un generatore di test, puoi rieseguirlo per aggiornare/sincronizzare l'esercizio con i suoi ultimi dati canonici. Consigliamo di farlo periodicamente, per controllare se ci sono casi di test problematici da aggiornare o nuovi test che potresti voler includere.

Punto di partenza

Ci sono due possibili punti di partenza quando si implementa un generatore di test per un esercizio:

  1. L'esercizio è nuovo e quindi non ha alcun test
  2. L'esercizio esiste già e quindi ha test esistenti
Caution

Se ci sono test esistenti, implementa il generatore di test in modo che i test che genera non rompano le soluzioni esistenti.

Progettazione

In linea di massima, i file di test vengono generati usando:

  • Codice: i file di test sono (per lo più) generati tramite codice
  • Template: i file di test sono (per lo più) generati usando dei template

Abbiamo notato che l'approccio basato sul codice porta a un codice del generatore di test piuttosto complesso, mentre l'approccio basato sui template è più semplice.

Quello che consigliamo è il seguente flusso:

  1. Leggere i dati canonici dell'esercizio
  2. Escludere i casi di test contrassegnati come include = false nel file tests.toml dell'esercizio
  3. Convertire i dati canonici dell'esercizio in un formato utilizzabile in un template
  4. Passare i dati canonici dell'esercizio a un template specifico per l'esercizio

Il vantaggio principale di questa configurazione è che ogni esercizio ha il proprio template, il che:

  • Rende evidente come vengono generati i file di test
  • Li rende più facili da correggere
  • Rende sicuro modificarli senza rischiare di rompere un altro esercizio
Caution

Quando progetti il generatore di test, cerca di:

  • Ridurre al minimo la pre-elaborazione dei dati canonici all'interno del generatore di test
  • Ridurre l'accoppiamento tra i template

Implementazione

Il generatore di test è di solito (per lo più) scritto nel linguaggio del track.

Caution

Sebbene tu sia libero di usare altri linguaggi, ogni linguaggio aggiuntivo renderà più difficile mantenere o contribuire al track. Perciò ti consigliamo di usare il linguaggio del track dove possibile, perché rende più facile mantenerlo o contribuirvi.

Formattazione

Se il tuo track ha strumenti per formattare il codice, valuta di eseguirli come passaggio di post-elaborazione dopo aver renderizzato il tuo template.

Dati canonici

I dati principali con cui lavora il generatore di test sono il file canonical-data.json di un esercizio. Questo file è definito nel repository exercism/problem-specifications, che definisce i metadati condivisi di molti esercizi di Exercism.

Caution

Non tutti gli esercizi hanno un file canonical-data.json! Se non ce l'hanno, dovrai creare manualmente i test, perché non ci sono dati con cui il generatore di test possa lavorare.

Struttura

I dati canonici sono definiti in un oggetto JSON. Questo oggetto contiene un campo "cases" che contiene i casi di test. Questi casi di test (normalmente) corrispondono uno a uno ai test del tuo track.

Ogni caso di test ha un paio di proprietà, tra cui le più importanti sono la descrizione, la proprietà, i valori di input e il valore atteso. Ecco un esempio (parziale) del file canonical-data.json dell'esercizio 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
    }
  ]
}

Il compito principale del generatore di test è trasformare questi dati JSON in test specifici del track. Ecco come il JSON qui sopra potrebbe tradursi in codice di 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 struttura del file canonical-data.json è ben documentata e ha anche una definizione di schema JSON.

Annidamento

Alcuni esercizi usano l'annidamento nei loro dati canonici. Questo significa che ogni elemento di un array cases può essere:

  1. Un caso di test normale (senza casi di test figli)
  2. Un raggruppamento di casi di test (uno o più casi di test figli)
Note

Puoi identificare il tipo di un elemento controllando la presenza di campi esclusivi di un tipo di elemento. Probabilmente il modo migliore per farlo è usare la chiave "cases", che è presente solo nei gruppi di casi di test.

Ecco un esempio di casi di test annidati:

{
  "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

Se il tuo track non supporta il raggruppamento dei test, dovrai:

  • Attraversare/appiattire la gerarchia cases per ottenere solo i casi di test più interni (foglia)
  • Combinare la descrizione del caso di test con quella dei suoi genitori per creare un nome di test univoco

Valori di input e attesi

Il contenuto delle chiavi input e expected di un caso di test varia molto. Nella maggior parte dei casi saranno valori scalari (come numeri, booleani o stringhe) oppure oggetti semplici. Tuttavia, a volte troverai anche valori più complessi che probabilmente richiederanno un po' di pre-elaborazione, come funzioni lambda in pseudo codice, liste di operazioni da eseguire sul codice degli studenti e altro ancora.

Scenari

I casi di test hanno un campo opzionale scenarios. Questo campo può essere usato dal generatore di test per trattare in modo speciale certi casi di test. Il caso d'uso più comune è ignorare certi tipi di test, per esempio i test con lo scenario "unicode", dato che il linguaggio del tuo track potrebbe non supportare Unicode.

L'elenco completo degli scenari si trova qui.

Leggere i file canonical-data.json

Ci sono un paio di opzioni per leggere i file canonical-data.json:

  1. Recuperarli direttamente dal repository problem-specifications (ad esempio https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json).
  2. Aggiungere il repo problem-specifications come sottomodulo Git al repo del track.
  3. Leggerli dalla cache di configlet. La posizione dipende dal sistema dell'utente, ma puoi usare configlet info -o -v d | head -1 | cut -d " " -f 5 per ottenere la posizione a livello di codice.

Casi di test specifici del track

Se il tuo track desidera aggiungere alcuni casi di test aggiuntivi, specifici del track (che non si trovano nei dati canonici), un'opzione è creare un file additional-test-cases.json, che il generatore di test può poi unire al file canonical-data.json prima di passarlo al template per il rendering.

Template

Il motore di template da usare sarà probabilmente specifico del track. Idealmente, vorrai che i tuoi template siano il più semplici possibile, quindi non preoccuparti della duplicazione del codice e simili.

I template stessi riceveranno i loro dati dal generatore di test, sul quale iterano per renderizzarli.

Note

Per aiutare a mantenere semplici i template, potrebbe essere utile fare un po' di pre-elaborazione dal lato del generatore di test, oppure definire alcuni «filtri» o qualunque meccanismo di estensione consentano i tuoi template.

Usare configlet

configlet è lo strumento principale per la manutenzione dei track e può essere usato per:

  • Creare i file di esercizio per un nuovo esercizio: esegui bin/configlet create --practice-exercise <slug>
  • Sincronizzare il file tests.toml di un esercizio esistente: esegui bin/configlet sync --tests --update --exercise <slug>
  • Recuperare su disco i dati canonici dell'esercizio (questo è un effetto collaterale di uno dei comandi precedenti)

Questo rende configlet uno strumento fantastico da usare in combinazione con il generatore di test per flussi di lavoro davvero potenti.

Interfaccia a riga di comando

Vorrai rendere l'uso del generatore di test sia facile sia potente. Per questo, ti consigliamo di creare uno o più file di script.

Note

Sei libero di scegliere il formato di file di script che si adatta meglio al tuo track. Gli script shell e gli script PowerShell sono opzioni comuni che possono funzionare bene entrambe.

Ecco un esempio di script shell che combina configlet e un generatore di test per creare rapidamente lo scheletro di un nuovo esercizio:

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

Costruire da zero

Prima di iniziare a costruire un generatore di test, ti suggeriamo di dare un'occhiata a un paio di generatori di test esistenti per capire come altri track li hanno implementati:

Se hai delle domande, il forum è il posto migliore dove farle. Anche le discussioni sul forum sul generatore di test di Rust e su quello di JavaScript potrebbero esserti utili.

Prodotto minimo funzionante

Consigliamo di costruire il generatore di test in modo incrementale, partendo da un prodotto minimo funzionante. Una versione davvero minima leggerebbe il canonical-data.json di un esercizio e passerebbe semplicemente quei dati al template.

Inizia concentrandoti su un singolo esercizio, preferibilmente uno semplice come leap. Solo quando lo avrai funzionante dovresti aggiungere gradualmente altri esercizi.

E cerca di mantenere il generatore di test il più semplice possibile.

Note

Idealmente, chi contribuisce potrebbe semplicemente incollare/modificare un template esistente senza dover capire come funziona internamente il generatore di test.

Usare o contribuire

Come usare o contribuire a un generatore di test dipende dal track. Cerca le istruzioni nel README.md del track, nel CONTRIBUTING.md o nella directory del codice del generatore di test.