Ein Test-Generator ist ein Track-spezifisches Stück Software, das die Tests einer Übung automatisch generiert. Dazu wandelt er die JSON-Testfälle der Übung in Tests in der Sprache des Tracks um.
Ein Test-Generator hat unter anderem diese Vorteile:
Im Allgemeinen führt man einen Test-Generator aus, um entweder:
Wenn du für eine neue Übung einen Test-Generator hinzufügst, kannst du ihre Testdatei(en) generieren. Vorausgesetzt, der Test-Generator selbst ist schon implementiert, ist das Generieren der Tests für die neue Übung (deutlich) weniger Arbeit, als sie von Grund auf zu schreiben.
Sobald eine Übung einen Test-Generator hat, kannst du ihn erneut ausführen, um die Übung mit ihren neuesten kanonischen Daten zu aktualisieren bzw. abzugleichen. Wir empfehlen, das regelmäßig zu machen, um zu prüfen, ob es problematische Testfälle gibt, die aktualisiert werden müssen, oder neue Tests, die du aufnehmen möchtest.
Es gibt zwei mögliche Ausgangspunkte, wenn du einen Test-Generator für eine Übung implementierst:
Wenn es bereits Tests gibt, implementiere den Test-Generator so, dass die von ihm generierten Tests keine bestehenden Lösungen kaputt machen.
Grob gesagt werden Testdateien auf eine von zwei Arten generiert:
Wir haben festgestellt, dass der codebasierte Ansatz zu recht komplexem Test-Generator-Code führt, während der Template-Ansatz einfacher ist.
Wir empfehlen folgenden Ablauf:
tests.toml-Datei der Übung als include = false markiert sindDer entscheidende Vorteil dieses Aufbaus ist, dass jede Übung ihr eigenes Template hat, was:
Versuche beim Entwerfen des Test-Generators:
Der Test-Generator ist normalerweise (größtenteils) in der Sprache des Tracks geschrieben.
Du kannst zwar auch andere Sprachen verwenden, aber jede zusätzliche Sprache macht es schwieriger, den Track zu pflegen oder zu ihm beizutragen. Deshalb empfehlen wir, wo möglich die Sprache des Tracks zu verwenden, weil das die Pflege und das Mitwirken einfacher macht.
Wenn dein Track Tools zum Formatieren von Code hat, kannst du sie als Nachbearbeitungsschritt nach dem Rendern deines Templates ausführen.
Die zentrale Datenquelle, mit der der Test-Generator arbeitet, ist die canonical-data.json-Datei einer Übung.
Diese Datei ist im Repository exercism/problem-specifications definiert, das gemeinsame Metadaten für viele Übungen von Exercism festlegt.
Nicht alle Übungen haben eine canonical-data.json-Datei!
Wenn nicht, musst du die Tests manuell erstellen, weil es keine Daten gibt, mit denen der Test-Generator arbeiten kann.
Kanonische Daten sind in einem JSON-Objekt definiert.
Dieses Objekt enthält ein "cases"-Feld, das die Testfälle enthält.
Diese Testfälle entsprechen (normalerweise) eins zu eins den Tests in deinem Track.
Jeder Testfall hat ein paar Eigenschaften, wobei die Felder description, property, input value(s) und expected value die wichtigsten sind.
Hier ist ein (teilweises) Beispiel der canonical-data.json-Datei der Übung 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
}
]
}
Die Hauptaufgabe des Test-Generators ist es, diese JSON-Daten in Track-spezifische Tests umzuwandeln. So könnte das obige JSON in Nim-Testcode übersetzt werden:
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
Die Struktur der canonical-data.json-Datei ist gut dokumentiert und es gibt auch eine Definition als JSON-Schema.
Manche Übungen verwenden Verschachtelung in ihren kanonischen Daten.
Das bedeutet, dass jedes Element in einem cases-Array Folgendes sein kann:
Du kannst die Art eines Elements daran erkennen, ob Felder vorhanden sind, die nur für einen Elementtyp vorkommen.
Am besten geht das wahrscheinlich über den Schlüssel "cases", der nur in Testfallgruppen vorkommt.
Hier ist ein Beispiel für verschachtelte Testfälle:
{
"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
}
}
]
}
]
}
Wenn dein Track das Gruppieren von Tests nicht unterstützt, musst du:
cases-Hierarchie durchlaufen bzw. flach machen, sodass am Ende nur die innersten (Blatt-)Testfälle übrig bleibenDer Inhalt der Testfall-Schlüssel input und expected ist sehr unterschiedlich.
In den meisten Fällen sind es skalare Werte (wie Zahlen, boolesche Werte oder Strings) oder einfache Objekte.
Gelegentlich findest du aber auch komplexere Werte, die wahrscheinlich etwas Vorverarbeitung erfordern, zum Beispiel Lambdas in Pseudocode, Listen von Operationen, die auf dem Code der Lernenden ausgeführt werden sollen, und mehr.
Testfälle haben ein optionales Feld scenarios.
Dieses Feld kann vom Test-Generator verwendet werden, um bestimmte Testfälle gesondert zu behandeln.
Der häufigste Anwendungsfall ist, bestimmte Testarten zu ignorieren, zum Beispiel Tests mit dem Szenario "unicode", weil die Sprache deines Tracks Unicode möglicherweise nicht unterstützt.
Die vollständige Liste der Szenarien findest du hier.
Es gibt mehrere Möglichkeiten, die canonical-data.json-Dateien zu lesen:
problem-specifications abrufen (z. B. https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json).problem-specifications als Git-Submodul zum Track-Repository hinzufügen.configlet lesen.
Der Speicherort hängt vom System des Nutzers ab, aber du kannst configlet info -o -v d | head -1 | cut -d " " -f 5 verwenden, um den Speicherort programmatisch zu ermitteln.Wenn dein Track zusätzliche, Track-spezifische Testfälle hinzufügen möchte (die es in den kanonischen Daten nicht gibt), kannst du zum Beispiel eine additional-test-cases.json-Datei erstellen, die der Test-Generator dann mit der canonical-data.json-Datei zusammenführen kann, bevor er sie zum Rendern an das Template übergibt.
Die verwendete Template-Engine ist wahrscheinlich Track-spezifisch. Am besten hältst du deine Templates so einfach wie möglich, also mach dir keine Gedanken über Code-Duplizierung und Ähnliches.
Die Templates selbst bekommen ihre Daten vom Test-Generator; er iteriert über diese Daten, um sie zu rendern.
Um die Templates einfach zu halten, kann es hilfreich sein, auf der Seite des Test-Generators etwas Vorverarbeitung zu betreiben oder ein paar „Filter“ zu definieren, oder welchen Erweiterungsmechanismus deine Templates auch immer unterstützen.
configlet ist das wichtigste Werkzeug zur Pflege eines Tracks und kann verwendet werden, um:
bin/configlet create --practice-exercise <slug> austests.toml-Datei einer bestehenden Übung abzugleichen: Führe bin/configlet sync --tests --update --exercise <slug> ausDamit ist configlet ein großartiges Tool, das du in Kombination mit dem Test-Generator für richtig leistungsstarke Workflows einsetzen kannst.
Du möchtest die Verwendung des Test-Generators sowohl einfach als auch leistungsstark machen. Dafür empfehlen wir, eine oder mehrere Skriptdateien zu erstellen.
Du kannst frei wählen, welches Skriptdateiformat am besten zu deinem Track passt. Shell-Skripte und PowerShell-Skripte sind gängige Optionen, die beide gut funktionieren können.
Hier ist ein Beispiel für ein Shell-Skript, das configlet und einen Test-Generator kombiniert, um schnell eine neue Übung aufzusetzen:
bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>
Bevor du anfängst, einen Test-Generator zu bauen, schau dir am besten ein paar bestehende Test-Generatoren an, um ein Gefühl dafür zu bekommen, wie andere Tracks sie umgesetzt haben:
Wenn du Fragen hast, ist das Forum der beste Ort, um sie zu stellen. Auch die Forendiskussionen rund um den Rust und die JavaScript-Test-Generatoren können hilfreich sein.
Wir empfehlen, den Test-Generator schrittweise aufzubauen und mit einem Minimal Viable Product zu beginnen.
Eine absolute Minimalversion würde die canonical-data.json einer Übung lesen und diese Daten einfach an das Template weitergeben.
Konzentriere dich zuerst auf eine einzige Übung, am besten eine einfache wie leap.
Erst wenn das funktioniert, solltest du nach und nach weitere Übungen hinzufügen.
Und versuche, den Test-Generator so einfach wie möglich zu halten.
Am besten könnte ein Mitwirkender einfach ein bestehendes Template einfügen bzw. anpassen, ohne verstehen zu müssen, wie der Test-Generator intern funktioniert.
Wie du einen Test-Generator verwendest oder zu ihm beiträgst, ist Track-spezifisch.
Schau nach Anweisungen in der README.md, der CONTRIBUTING.md des Tracks oder im Verzeichnis des Test-Generator-Codes.