Die Test-Runner-Schnittstelle


Test Runner haben eine einzige Aufgabe: Sie nehmen eine Lösung entgegen, führen alle Tests aus und geben eine standardisierte Ausgabe zurück. Die gesamte Interaktion mit der Exercism-Website läuft automatisch ab und ist nicht Teil dieser Spezifikation.

Ausführung

  • Ein Test Runner sollte ein ausführbares Skript bereitstellen. Mehr Informationen findest du in der Datei docker.md.
  • Das Skript erhält drei Parameter:
    • Den Slug der Übung (z. B. two-fer).
    • Einen Pfad zu einem Eingabeverzeichnis (mit abschließendem Schrägstrich), das die eingereichten Lösungsdatei(en) und alle anderen Übungsdatei(en) enthält. Dieses Verzeichnis solltest du als schreibgeschützt betrachten. Technisch ist es möglich, hineinzuschreiben, aber besser ist es, /tmp für temporäre Dateien zu verwenden (z. B. zum Kompilieren von Quellcode).
    • Einen Pfad zu einem Ausgabeverzeichnis (mit abschließendem Schrägstrich). In dieses Verzeichnis kannst du schreiben.
  • Das Skript muss eine Datei results.json in das Ausgabeverzeichnis schreiben.
  • Der Runner muss mit einem Exit-Code von 0 enden, wenn er erfolgreich gelaufen ist, unabhängig vom Status der Tests.

Erlaubte Laufzeit

Der Test Runner erhält 100 % CPU und 3 GB Arbeitsspeicher für ein Zeitfenster von 20 Sekunden pro Lösung. Nach 20 Sekunden wird der Prozess angehalten und meldet einen Timeout.

Note

Wir empfehlen dir dringend, unser Dokument mit den Best Practices zur Performance zu befolgen, um die Wahrscheinlichkeit von Timeouts zu verringern.

Ausgabeformat

Die folgenden Felder werden in results.json-Dateien unterstützt:

Oberste Ebene

Version

Schlüssel: version, Typ: number, Präsenz: erforderlich

version: 1, 2, 3

Die Version der Spezifikation, an die sich diese Datei hält:

  • 1: Für Tracks, deren Test Runner keine Informationen zu einzelnen Tests liefern kann.
  • 2: Für Tracks, deren Test Runner Informationen zu einzelnen Tests ausgeben kann. Minimal erforderliche Version für Tracks mit Concept Exercises.
  • 3: Für Tracks, deren Test Runner einzelne Tests mit einer Aufgabe verknüpfen kann.

Status

Schlüssel: status, Typ: string, Präsenz: erforderlich

version: 1, 2, 3

Die folgenden Gesamtstatus sind gültig:

  • pass: Alle Tests bestanden
  • fail: Mindestens ein Test hat den Status fail oder error
  • error: Kein Test wurde ausgeführt (das bedeutet meist einen Kompilierfehler oder einen Syntaxfehler)

Der Status error sollte nur verwendet werden, wenn bei allen Tests ein Fehler auftrat. Bei kompilierten Sprachen ist das in der Regel die Folge davon, dass der Code nicht kompiliert werden kann. Bei interpretierten Sprachen ist das ein Laufzeitfehler, etwa ein Syntaxfehler, der das Parsen der Datei verhindert.

Nachricht

Schlüssel: message, Typ: string, Präsenz: erforderlich, wenn status = error, oder wenn status = fail und version = 1

version: 1, 2, 3

Wenn der Status error ist (kein Test wurde korrekt ausgeführt), sollte der message-Schlüssel auf oberster Ebene angegeben werden. Er sollte dem Nutzer den aufgetretenen Fehler mitteilen. Da es die einzige Information ist, die ein Nutzer zur Fehlersuche erhält, muss er so klar wie möglich sein:

  • Vereinfache die Pfade zu etwas wie <solution-dir>/relative/path statt /full/path/to, da Letzteres nutzlose, ECR-spezifische Daten enthält
  • Fasse Stacktraces, die nicht aus dem Nutzercode stammen, zusammen, wenn möglich oder anwendbar
  • Zeige niemals Callstacks ohne Kontext (d. h. ohne die Fehlermeldung)
  • Ändere die Fehlermeldung nicht (wenn möglich), denn das erleichtert die Suche nach dem Fehler

In Ruby geben wir bei einem Syntaxfehler den Laufzeitfehler und den Stacktrace an. Bei kompilierten Sprachen sollte der Kompilierfehler angegeben werden.

Der Wert von message auf oberster Ebene ist auf 65535 Zeichen beschränkt. Die effektive maximale Länge ist geringer, wenn der Wert Mehrbyte-Zeichen enthält.

Wenn der Status nicht error ist, setze den Wert entweder auf null oder lasse den Schlüssel ganz weg.

Tests

Schlüssel: tests, Typ: array, Präsenz: erforderlich, wenn status = fail oder status = pass

version: 2, 3

Dies ist ein Array der Testergebnisse, wie im Abschnitt „Pro Test" weiter unten beschrieben.

Die Tests MÜSSEN in der Reihenfolge zurückgegeben werden, in der sie in der Testdatei angegeben sind. Bei Sprachen, die Tests in zufälliger Reihenfolge ausführen, kann das bedeuten, dass du die Ergebnisse an die in der Testdatei angegebene Reihenfolge anpassen musst.

Der Grund dafür ist, dass Lernenden nur der erste Fehlschlag angezeigt wird, und deshalb ist es wichtig, dass der richtige Fehlschlag angezeigt wird. Da die Tests in der Testdatei in der Regel nach TDD geordnet sind und die Lernenden bei Practice Exercises die Testdatei im Editor sehen, ist es entscheidend, die Ergebnisse an die Testdatei anzugleichen.

Pro Test

Name

Schlüssel: name, Typ: string, Präsenz: erforderlich

version: 2, 3

Dies ist der Name des Tests in einem menschenlesbaren Format.

Testcode

Schlüssel: test_code, Typ: string, Präsenz: erforderlich, wenn die Übung eine Concept Exercise ist

version: 2, 3

Dies MUSS bei Concept Exercises vorhanden sein und SOLLTE bei Practice Exercises vorhanden sein. Der Unterschied bei dieser Anforderung kommt daher, dass den Lernenden bei Concept Exercises die Tests nicht angezeigt werden, sodass das Lösen der Übung ohne die Anzeige des test_code unmöglich sein kann, während die Tests bei Practice Exercises angezeigt werden.

Dies ist der Rumpf des Befehls, der getestet wird. Zum Beispiel sollte der folgende Ruby-Test:

def test_duplicate_items_uniqs_list
  cart = ShoppingCart.new
  cart.add(:STARIC)
  cart.add(:MEDNEW)
  cart.add(:MEDNEW)
  assert_equal 'Newspaper, Rice', cart.items_list
end

einen test_code-Wert von:

"cart = ShoppingCart.new
cart.add(:STARIC)
cart.add(:MEDNEW)
cart.add(:MEDNEW)
assert_equal 'Newspaper, Rice', cart.items_list"

liefern (wobei Zeilenumbrüche durch \n ersetzt werden, damit das JSON gültig ist).

Status

Schlüssel: status, Typ: string, Präsenz: erforderlich

version: 2, 3

Die folgenden Status pro Test sind gültig:

  • pass: Der Test war erfolgreich
  • fail: Der Test ist fehlgeschlagen
  • error: Der Test ist mit einem Fehler abgebrochen, das heißt, er hat keinen Wert zurückgegeben

Nachricht

Schlüssel: message, Typ: string, Präsenz: erforderlich, wenn status fail oder error ist

version: 2, 3

Der message-Schlüssel pro Test wird verwendet, um die Ergebnisse eines Tests mit dem status fail oder error zurückzugeben. Er sollte so menschenlesbar wie möglich sein. Was hier geschrieben wird, wird dem Lernenden angezeigt, wenn der Test nicht besteht. Wenn es keine Fehlschlagmeldung oder Fehlermeldung für den Test gibt, setze den Wert entweder auf null oder lasse den Schlüssel ganz weg. Es ist auch zulässig, hier die Ausgabe der Testsuite auszugeben. Der Wert von message ist in seiner Länge nicht begrenzt.

Ausgabe

Schlüssel: output, Typ: string, Präsenz: optional

version: 2, 3

Der output-Schlüssel pro Test sollte verwendet werden, um alles zu speichern und auszugeben, was ein Nutzer absichtlich für einen Test ausgibt.

  • Er sollte allen Testergebnissen angehängt werden, die Nutzerausgaben erzeugen.
  • Nur Inhalte, die ein Nutzer manuell ausgegeben hat, sollten angezeigt werden, nicht die automatische Ausgabe des Test Runners.
  • Du kannst entweder Inhalte erfassen, die auf normalem Weg ausgegeben werden (z. B. puts in Ruby, print in Python oder Debug.WriteLine in C#), oder eine Methode bereitstellen, die der Nutzer verwenden kann (z. B. stellt der Ruby Test Runner dem Nutzer eine global verfügbare debug-Methode zur Verfügung, die er verwenden kann und die dieselben Eigenschaften wie die Standardmethode puts hat).
  • Die Ausgabe muss auf 500 Zeichen begrenzt sein. Es ist akzeptabel, sie entweder mit der Meldung „Output was truncated. Please limit to 500 chars" abzuschneiden oder in dieser Situation einen Fehler zurückzugeben.

Task-ID

Schlüssel: task_id, Typ: number, Präsenz: optional

version: 3

Verknüpfe einen Test über die ID der Aufgabe mit einer bestimmten Aufgabe. Die ID ist die Nummer, die am Anfang der Aufgabenüberschrift steht. Verknüpfe einen Test nur dann mit einer Aufgabe, wenn er sich genau einer Aufgabe zuordnen lässt.

Derzeit haben nur Concept Exercises klar definierte Aufgaben, mit denen du Tests verknüpfen kannst, aber das könnte sich in Zukunft ändern.

Betrachte zum Beispiel die folgende Datei instructions.md:

# Instructions

You're going to write some code to help Lucian cook an exquisite lasagna from his favorite cook book.

## 1. Define the expected oven time in minutes

...

## 2. Calculate the remaining oven time in minutes

...

Diese Anweisungen definieren zwei Aufgaben:

  1. Die erwartete Backofenzeit in Minuten definieren
  2. Die verbleibende Backofenzeit in Minuten berechnen

Die Datei results.json könnte dann einen Eintrag wie diesen enthalten:

{
  "name": "Expected oven time in minutes",
  "status": "pass",
  "task_id": 1,
  "test_code": "Assert.Equal(40, Lasagna.ExpectedMinutesInOven());"
}

Dieser Test ist jetzt mit der ersten Aufgabe verknüpft: „Die erwartete Backofenzeit in Minuten definieren". Beachte, dass der Name nicht mit der Beschreibung der Aufgabe übereinstimmen muss.

Es gibt verschiedene Möglichkeiten, wie Tracks das umsetzen könnten:

  • Füge den Tests in der Testdatei Metadaten hinzu (z. B. über Attribute, Annotationen oder Kommentare) und lass den Test Runner diese Metadaten beim Ausführen der Tests lesen.
  • Speichere die Zuordnung von Testname zu Task-ID in einer separaten Datei (etwa der Datei .meta/config.json der Übung) und führe diese Informationen mit der erzeugten Datei results.json zusammen.

Beispiele

Dies sind Beispiele dafür, wie eine gültige Datei results.json für die verschiedenen Versionen aussehen kann:

Beispiel für v1

{
  "version": 1,
  "status": "fail",
  "message": "Failed: test_answer\nExpected: 42, actual: 3"
}

Beispiel für v2

{
  "version": 2,
  "status": "fail",
  "message": null,
  "tests": [
    {
      "name": "Test that the thing works",
      "status": "fail",
      "message": "Expected 42 but got 123123",
      "output": "Debugging information output by the user",
      "test_code": "assert_equal 42, answerToTheUltimateQuestion()"
    }
  ]
}

Beispiel für v3

{
  "version": 3,
  "status": "fail",
  "message": null,
  "tests": [
    {
      "name": "Test that the thing works",
      "status": "fail",
      "message": "Expected 42 but got 123123",
      "output": "Debugging information output by the user",
      "test_code": "assert_equal 42, answerToTheUltimateQuestion()",
      "task_id": 1
    }
  ]
}

UI/UX-Aspekte

Bei fehlgeschlagenem Test

Wenn die Lösung eines Lernenden bei einem Test fehlschlägt, sollte etwa Folgendes angezeigt werden:

Test Code:
  <test_code>

Test Result:
  <message>

Bei bestandenem Test

Wenn die Lösung einen Test besteht, sollte etwa Folgendes angezeigt werden:

Test Code:
  <test_code>

Wie du Metadaten für die Testsuite deiner Sprache hinzufügst

Alle Wege führen nach Rom, und es gibt kein vorgeschriebenes Vorgehen, um dorthin zu gelangen. Bislang wurden mehrere Ansätze gewählt:

  • Zusätzliche JSON-Dateien, die manuell zusammengestellt und zur Testlaufzeit mit den Testergebnissen zusammengeführt werden.
  • Automatisierte statische Analyse der Testsuite, die zur Testlaufzeit mit den Testergebnissen zusammengeführt wird.
    • Das kann durch AST-Analyse oder Textparsing erreicht werden.