Antra

Werkzeuge

Testing mit Antra

Dieses Kapitel zeigt, wie du heute in Antra kleine und mittlere Tests mit Antra::Test schreibst.

Der Fokus ist bewusst praktisch:

  • Wie sieht ein einzelner Test aus?
  • Wie starte ich mehrere Tests?
  • Wie benenne und gruppiere ich Tests lesbar?
  • Wie pruefe ich Fehlerfaelle?

Die Kurzversion#

Wenn du nur die wichtigste Arbeitsregel willst:

  • Fuer einzelne explizite Faelle nutze Test.case(...) und Test.run_tests(...).
  • Fuer Package-basierte Tests nutze @Test(...) und Test.run_test(...).
  • Verwende *.t.ant fuer ausfuehrbare Test-Runner.
  • Lege importierbare Test-Pakete ruhig unter t/lib/... ab.
  • Gib Assertions nach Moeglichkeit eine klare Fehlermeldung mit.

Fuer viele Projekte reicht das bereits weit.

Ein erster kleiner Test#

Die kleinste Form ist ein expliziter Testfall mit Test.case(...).

import Antra::Test as Test

val cases = [
    Test.case("eins plus eins", || -> Test.assert_eq(1 + 1, 2)),
    Test.case("wahr bleibt wahr", || -> Test.assert_true(true))
]

Test.run_tests(cases)

Die Idee ist einfach:

  • jeder Test hat einen Namen
  • jeder Test ist eine nullstellige Funktion
  • run_tests(...) fuehrt alle Faelle aus und druckt eine kleine TAP-artige Ausgabe

Wenn ein Test fehlschlaegt, wirft der Runner am Ende selbst einen Fehler. Damit eignet er sich gut fuer Skripte, CI und Shell-Aufrufe.

Die wichtigsten Assertions#

Antra::Test ist absichtlich klein. Die wichtigsten Helfer sind:

Test.assert_true(value)
Test.assert_true(value, message)

Test.assert_eq(actual, expected)
Test.assert_eq(actual, expected, message)

Test.assert_error(expr, needle)
Test.assert_error(expr, needle, message)

assert_true#

Test.assert_true(len(users) > 0)
Test.assert_true(ready, "expected system to be ready")

assert_eq#

Test.assert_eq(sum([1, 2, 3]), 6)
Test.assert_eq(user.role, :admin, "expected default role to be :admin")

assert_error#

assert_error(...) prueft, dass ein Ausdruck wirft und dass die Fehlermeldung einen bestimmten Text enthaelt.

Wichtig: das erste Argument ist jetzt ein on_use-Parameter. Du uebergibst also direkt den Ausdruck, der spaeter innerhalb von assert_error(...) ausgewertet werden soll.

on_use sorgt dafuer, dass der Ausdruck nicht schon vor dem Aufruf ausgewertet wird — sonst wuerde parse("???").! bereits werfen, bevor assert_error(...) ihn pruefen kann. Darum ist heute diese Form richtig:

Test.assert_error(parse("???").!, "unexpected token")

und die alte Closure-Form ist fuer diesen Helfer nicht mehr noetig:

Test.assert_error(|| -> parse("???").!, "unexpected token")

Was on_use genau ist (Semantik, moegliche Mehrfach-Auswertung, Regeln) erklaert Kapitel 08 — fuer assert_error(...) reicht: Ausdruck direkt uebergeben, der Helfer wertet ihn kontrolliert aus.

Mit eigener Meldung:

Test.assert_error(
    parse("???").!,
    "unexpected token",
    "expected parser to reject invalid syntax"
)

Wenn du assert_error(...) in einer Pipe nutzen willst, beachte die normale Pipe-Regel: der linke Ausdruck landet immer auf der ersten Argumentposition.

parse("???").! |> Test.assert_error("unexpected token")

Das funktioniert hier, weil assert_error(...) den on_use-Parameter an erster Stelle hat.

Warum eigene Fehlermeldungen oft sinnvoll sind#

Die Standardmeldungen sind fuer kleine Tests ok. In echten Test-Suites lohnt es sich aber oft, die Absicht explizit mitzuschreiben.

Statt nur:

Test.assert_eq(after.castling, ["k", "q"])

ist das oft lesbarer:

Test.assert_eq(
    after.castling,
    ["k", "q"],
    "expected white castling rights to be cleared after white castles king-side"
)

Wenn der Test spaeter fehlschlaegt, ist der Output meist deutlich schneller zu verstehen.

Von expliziten Cases zu Package-Tests#

Sobald mehrere Tests zusammengehoeren, ist die Package-Form meist angenehmer.

package Demo::Tests::MathTest {
    import Antra::Test as Test

    @Test("adds two numbers")
    pub fn adds_numbers_test() -> {
        Test.assert_eq(1 + 2, 3)
    }

    @Test("multiplies two numbers")
    pub fn multiplies_numbers_test() -> {
        Test.assert_eq(2 * 3, 6)
    }
}

Dann reicht ein Runner:

import Antra::Test as Test
import Demo::Tests::MathTest as MathTests

Test.run_test(MathTests)

Wie run_test(...) Tests entdeckt#

run_test(...) entdeckt heute Tests nach einfachen Regeln:

  • jede exportierte Funktion, deren Name auf _test endet
  • jede exportierte Funktion mit @Test(...)
  • @Test("...") ueberschreibt den angezeigten Testnamen

Das erlaubt zwei Stile.

Nur ueber den Namen:

pub fn parses_config_test() -> ...

Oder mit explizitem Label:

@Test("parses valid config files")
pub fn parses_config_test() -> ...

Die zweite Form ist fuer Nutzer-Doku und CI-Output meist lesbarer.

@TestSuite(...) und group:#

Wenn du Tests grober strukturieren willst, kannst du Suite- und Gruppen- Metadaten setzen.

@TestSuite(name: "UCI")
package Chess::Tests::UCITest {
    import Antra::Test as Test

    @Test(name: "parses coordinate moves", group: :parse)
    pub fn parses_coordinate_moves_test() -> {
        ...
    }

    @Test(name: "applies castling", group: :apply)
    pub fn applies_castling_test() -> {
        ...
    }

    @Test(name: "rejects invalid move forms", group: :validation)
    pub fn rejects_invalid_move_forms_test() -> {
        ...
    }
}

Das hat zwei Effekte:

  • die Ausgabe kann nach Suite und Gruppe lesbarer gegliedert werden
  • du kannst spaeter gezielt nur eine Gruppe laufen lassen

Zum Beispiel:

Test.run_test(UCITests, [group: :parse])

Oder ueber mehrere Packages:

Test.run_test([SANTests, UCITests], [group: :validation, colorize: true])

Ein typischer Projektaufbau#

Ein einfacher, heute gut funktionierender Aufbau ist:

t/
  run.t.ant
  lib/
    MyApp/
      Tests/
        ParserTest.ant
        EvalTest.ant

Ein Runner kann dann so aussehen:

import Antra::Test as Test
import MyApp::Tests::ParserTest as ParserTests
import MyApp::Tests::EvalTest as EvalTests

Test.run_test([ParserTests, EvalTests], [colorize: true])

Praktisch heisst das oft:

  • *.t.ant sind ausfuehrbare Einstiegspunkte
  • importierbare Test-Sammlungen bleiben normale .ant-Packages
  • Paketnamen wie ...::Tests::... machen im Editor und in Runnern schneller klar, was Produktionscode und was Testcode ist

Beispiel fuer einen kompletten Test-Runner#

import Antra::Test as Test
import Chess::Tests::SANTest as SANTests
import Chess::Tests::UCITest as UCITests

Test.run_test([SANTests, UCITests], [colorize: true])

Mit Gruppenfilter:

Test.run_test([SANTests, UCITests], [group: :apply, colorize: true])

Wie die Ausgabe ungefaehr aussieht#

Der Runner bleibt bewusst klein und TAP-nah.

Ein moeglicher Lauf sieht ungefaehr so aus:

1..3
# [SAN]
# Apply:
ok 1 - SAN applies castling from a castle-ready position
ok 2 - SAN applies a short legal move sequence
# Parse:
ok 3 - SAN parses common move forms
# 3 passed; 0 failed

Wichtig dabei:

  • die Nummerierung laeuft global durch
  • Suite- und Gruppenzeilen sind Zusatzstruktur, keine eigene Zaehlebene
  • bei Fehlern kommt darunter noch die Detailmeldung des fehlgeschlagenen Tests

Wann run_tests(...) und wann run_test(...)?#

Als Faustregel:

  • run_tests(...) fuer kleine lokale Testsammlungen aus expliziten case(...)
  • run_test(...) fuer Package-basierte Test-Suites mit @Test(...)

run_tests(...) ist gut, wenn du etwas sehr direkt auflisten willst. run_test(...) ist besser, sobald Tests als normales Test-Package leben.

Was heute bewusst klein bleibt#

Antra::Test ist aktuell absichtlich kein grosses xUnit-Framework.

Was heute bewusst klein oder nicht vorhanden ist:

  • kein before_each
  • kein after_each
  • keine tief verschachtelten describe-Baeume
  • keine parallele Ausfuehrung
  • keine riesige Assertion-Bibliothek

Das ist kein Versehen. Die aktuelle Surface ist eher fuer klare, nachvollziehbare Bibliotheks- und Sprachtests gedacht.

Praktische Empfehlungen#

Zum Schluss ein paar robuste Alltagsregeln:

  • Gib Tests sprechende Namen, nicht nur technische Funktionsnamen.
  • Nutze bei nichttrivialen Assertions eigene Fehlermeldungen.
  • Gruppiere eher ueber Packages und group: als ueber riesige Einzeldateien.
  • Halte Test-Runner klein; importiere lieber mehrere Test-Packages.
  • Nutze assert_error(...) bei Fehlerfaellen mit klaren Teilstrings statt nur "irgendein Fehler ist passiert" zu pruefen.

Wenn du so startest, bekommst du schon mit wenig Infrastruktur gut lesbare Tests in Antra.

Quelle dieser Seite