Blog

Test2::V0

01.08.2020 // Gregor Goldbach

Perl hat eine lange Tradition im Testen. Klassischerweise werden Tests mit Hilfe der Distribution Test::More geschrieben und mit prove ausgeführt. Test::More wird jedoch seit längerem schon vielen Anforderungen nicht mehr gerecht.

Test2 ist eine Neuentwicklung eines Test-Frameworks, das diese Anforderungen erfüllt und für zukünftige Erweiterungen gerüstet ist. In diesem Artikel stellen wir dieses Framework vor.

Einleitung

In den letzten Jahren haben sich die Anforderungen an ein Test-Framework geändert. Die Verwendung von Test::More ist teilweise problematisch. Einige Probleme möchte ich hier aufführen:

  • Tests und das sie ausführende Programm kommunizieren über das Textprotokoll TAP (Test Anything Protocol). Dieses Protokoll ist eher für Maschinen als Menschen gemacht und teilweise schwer zu lesen. Zudem unterstützt es Subtests nicht vollständig.
  • Die Ausgabe von TAP ist in Tests mit normaler Ausgabe vermischt. Diagnosemeldungen können nur schlecht mit Testausgaben synchronisiert werden.
  • Erweiterungen sind nur schwer möglich, sodass die Weiterentwicklung mit neuen Features praktisch nicht stattfindet.
  • Best Practices im Schreiben von modernem Perl-Code sind boilerplate und tribal knowledge. Praktisch immer verwendete Argumente beim Aufruf von Tests müssen immer wieder angegeben werden und sind nicht voreingestellt.* *Das macht die Verwendung umständlich.
  • Das Ergebnis eines Testlaufs ist als Text in der Konsole zu sehen. Nur dort. Untersuchungen des Testlaufs kann man nur durch Inspektion dieser Ausgabe vornehmen. Es ist nicht möglich, das Ergebnis eines Testlaufs auf einem anderen Rechner zu untersuchen.

Test2 soll alles besser machen und erweiterbar sein. Zudem bietet es erweiterte Funktionalitäten und wird aktiv weiterentwickelt. Der Autor Chad Granum stellt seit 2015 wesentliche Neuentwicklungen auf der Perl Konferenz vor.

Test2 ist praktisch gut ausgreift, sodass Teile von Test::More dieses neue Framework bereits seit Jahren verwenden.

TAP und Test::More

TAP ist das Test Anything Protocol, das Testprogramme und deren Aufrufer verbindet. Es sind in der Ausgabe von Testprogrammen die Zeilen, die mit # beginnen. Sie werden von den Funktionen, die in den auf Test::Builder basierten Modulen implementiert sind, auf STDOUT ausgegeben. Außerdem gibt es Funktionen, die Diagnosemeldungen auf STDERR ausgeben.

Der Aufrufer von Testprogrammen ist bei Perl bisher in der Regel prove, das Test::Harness (Test-Harnisch) zum Parsen von TAP verwendet und bei Bedarf STDOUT und STDERR teilweise oder ganz ausgibt.

Die Tests werden mit Test::More implementiert, das wiederum auf Test::Builder aufsetzt. Große Tests können in kleinere Subtests unterteilt werden, die dann im TAP etwas eingerückt dargestellt werden.

Im Wesentlichen werden hier von dem aufrufenden prove Textausgaben verarbeitet.

Vorteile von Test2

Test2 bietet eine Reihe von Vorzügen, die ich hier nur in Teilen ausführen kann.

Das Design ist mit den Erfahrungen der letzten 30 Jahre Perl gewählt worden und macht Erweiterungen einfacher möglich. Wegen der einfacheren Arbeit wird Test2 im Gegensatz zu Test::Harness weiterentwickelt. Ein neueres Feature ist zum Beispiel das Setzen von Timeouts, nach dessen Überschreitung ein Test abgebrochen wird.

Die Standard-Testwerkzeuge wurden behutsam sinnvoll ergänzt, andere Module werden dadurch überflüssig. is und like können nun zum Beispiel auch Datenstrukturen vergleichen. Dafür musste bisher ein eigenes Testmodul geladen werden.

Ein kurzes Beispiel:

use Test2::V0;

my $some_hash = { a => 1, b => 2, c => 3 };

is(
    $some_hash,
    { a => 1, b => 2, c => 4 },
    "The hash we got matches our expectations"
);

done_testing;

Wenn man dieses Testprogramm mit dem prove-Ersatz yath aufruft, erhält man folgende Ausgabe:

$ yath test2-tools-compare-is.t 

** Defaulting to the 'test' command **

[  FAIL  ]  job  1  + The hash we got matches our expectations
[  DEBUG ]  job  1    test2-tools-compare-is.t line 5
(  DIAG  )  job  1    +------+-----+----+-------+
(  DIAG  )  job  1    | PATH | GOT | OP | CHECK |
(  DIAG  )  job  1    +------+-----+----+-------+
(  DIAG  )  job  1    | {c}  | 3   | eq | 4     |
(  DIAG  )  job  1    +------+-----+----+-------+
(  DIAG  )  job  1    Seeded srand with seed '20200721' from local date.
( FAILED )  job  1    test2-tools-compare-is.t
< REASON >  job  1    Test script returned error (Err: 1)
< REASON >  job  1    Assertion failures were encountered (Count: 1)

The following jobs failed:
+--------------------------------------+--------------------------+
| Job ID                               | Test File                |
+--------------------------------------+--------------------------+
| 3DC85D18-CB27-11EA-A9FD-3FD6B4ADC425 | test2-tools-compare-is.t |
+--------------------------------------+--------------------------+

                                Yath Result Summary
-----------------------------------------------------------------------------------
     Fail Count: 1
     File Count: 1
Assertion Count: 1
      Wall Time: 0.30 seconds
       CPU Time: 0.49 seconds (usr: 0.12s | sys: 0.03s | cusr: 0.27s | csys: 0.07s)
      CPU Usage: 165%
    -->  Result: FAILED  <--

Im Gegensatz zum alleinigen TAP kann Test2 beliebige Ausgaben erzeugen, die man als Plugins nachrüsten kann. TAP ist nur eine Variante.

Das Programm prove wurde durch yath (yet another test harness) ersetzt. Die damit erzeugte Ausgabe ist für Menschen lesbar, und nicht für Maschinen. yath protokolliert Testergebnisse als Ereignisse in Form von JSON in Dateien, die dann gut automatisiert verarbeitet werden können. Dieses Protokoll kannst du hinterher mit Bordmitteln von yath oder durch andere Programme untersuchen.

Es ist möglich, eine Testsuite mit yath ohne jegliche Ausgabe in zum Beispiel einer CI-Umgebung laufen zu lassen und das Protokoll des Testlaufs bei einem Fehlschlag beliebig oft zu untersuchen. Der folgende Aufruf ruft alle Tests im Verzeichnis t/ rekursiv auf und speichert das Protokoll des Testlaufs in komprimierter Form:

$ yath -q -B

** Defaulting to the 'test' command **


Wrote log file: /private/var/folders/cz/3j2yfjp51xq3sm7x1_f77wdh0000gn/T/2020-07-21_17:25:38_6E265890-CB66-11EA-B019-30DDB4ADC425.jsonl.bz2

The following jobs failed:
+--------------------------------------+-----------+
| Job ID                               | Test File |
+--------------------------------------+-----------+
| 6E274872-CB66-11EA-B019-30DDB4ADC425 | t/fail.t  |
+--------------------------------------+-----------+

                                Yath Result Summary
-----------------------------------------------------------------------------------
     Fail Count: 1
     File Count: 2
Assertion Count: 4
      Wall Time: 0.34 seconds
       CPU Time: 0.56 seconds (usr: 0.13s | sys: 0.03s | cusr: 0.31s | csys: 0.09s)
      CPU Usage: 162%
    -->  Result: FAILED  <--

Dieses Protokoll kann dann beispielsweise auf einen Entwicklerrechner heruntergeladen und dort untersucht werden. Beispielsweise kannst du die Ereignisse eines fehlgeschlagenen Tests über dessen UUID erneut abspielen, ohne den Test selbst laufen zu lassen. Du erhältst dann exakt die gleiche Ausgabe wie bei der Ausführung der Tests:

$ yath replay /private/var/folders/cz/3j2yfjp51xq3sm7x1_f77wdh0000gn/T/2020-07-21_17:25:38_6E265890-CB66-11EA-B019-30DDB4ADC425.jsonl.bz2 6E274872-CB66-11EA-B019-30DDB4ADC425
[  FAIL  ]  job  1  + The hash we got matches our expectations
[  DEBUG ]  job  1    t/fail.t line 5
(  DIAG  )  job  1    +------+-----+----+-------+
(  DIAG  )  job  1    | PATH | GOT | OP | CHECK |
(  DIAG  )  job  1    +------+-----+----+-------+
(  DIAG  )  job  1    | {c}  | 3   | eq | 4     |
(  DIAG  )  job  1    +------+-----+----+-------+
(  DIAG  )  job  1    Seeded srand with seed '20200721' from local date.
( FAILED )  job  1    t/fail.t
< REASON >  job  1    Test script returned error (Err: 1)
< REASON >  job  1    Assertion failures were encountered (Count: 1)

The following jobs failed:
+--------------------------------------+-----------+
| Job ID                               | Test File |
+--------------------------------------+-----------+
| 6E274872-CB66-11EA-B019-30DDB4ADC425 | t/fail.t  |
+--------------------------------------+-----------+

          Yath Result Summary
---------------------------------------
     Fail Count: 1
     File Count: 2
Assertion Count: 4
    -->  Result: FAILED  <--

STDOUT und STDERR können selbst bei Trennung von nebenläufig ausgeführten Tests über die erwähnten Ereignisse synchronisiert werden. Es ist also problemlos möglich, die Diagnosemeldungen einzelnen Tests zuzuordnen (das ist in obigem Beispiel über die angegebene Jobnummer zu erkennen). Insbesondere die Ausgabe von nebenläufig ausgeführten Tests kann weiterhin den Tests einfach zugeordnet werden. Das war mit prove bisher nicht möglich.

In der Distribution Test2::Harness::UI wird eine Webanwendung geliefert, mit der du die Ergebnisse des Testlaufs untersuchen kannst. Sie ist noch in einem frühen Entwicklungsstadium, funktioniert aber bereits. Beispiele dafür kannst du in einem Vortrag von seinem Autor Chad Granum sehen (YouTube-Video).

yath kann als Prozess laufen und Module vorladen, sodass sie nicht in jedem Testprogramm geladen werden müssen. Das bringt enorme Geschwindigkeitsvorteile – der Entwickler erhält schneller Rückmeldung und muss nicht auf Testergebnisse warten.

Testprogramm können mit Kommentaren versehen werden, die yath auswertet. Beispielsweise zeichnet der Kommentar #HARNESS-CAT-LONG ein Testprogramm als Langläufer aus, der bei nebenläufigem Aufruf vor den Kurzläufern gestartet wird, damit man nicht am Ende einer Testsuite noch auf den Langläufer warten muss.

Migrationspfad von Test::More zu Test2

Grundsätzlich sind Test2 und yath stabil und für den Produktiveinsatz geeignet.

Tatsächlich wird Test2 bereits seit Jahren von Test::More verwendet, da dies teilweise darin neu implementiert wurde (schau dir mal Test::Builder.pm an).

Eine Umstellung von Testprogrammen von Test::More auf Test2 ist in der Regel mit wenig Aufwand möglich. Es gibt dafür in der Test2-Distribution eine ausführliche Anleitung, in der die wenigen Inkompatibilitäten erklärt sind.

yath kannst du sofort ohne weiteren Aufwand als Ersatz für prove einsetzen.

Zum Weiterlesen und -sehen


Permalink: