How to ... Behat-Tests
Kursthemen
-
Was erwartet mich in diesem Kurs?
In diesem Kurs findet ihr einen Überblick zu Behat-Tests, darunter fällt die Ausführung von Tests sowie das Schreiben eigener Testfälle und Schritte.Behat ist ein Test-Framework für Behaviour Driven Development in PHP. Behat-Tests werden in der Beschreibungssprache Gherkin geschrieben, die eine natürliche Schriftsprache als Grundlage verwendet. Behat-Tests werden in Moodle für Acceptance-Testing verwendet, das heißt, dass Features aus User-Perspektive unter einem gegebenen Szenario geprüft werden. Im Moodle-Kontext kann es sich bei Usern sowohl um Lernende und Lehrende als auch um Admins handeln.
Der Kurs gliedert sich in folgende Abschnitte: -
Datei run.sh ausführen
Zum Durchführen der Tests muss lediglich die run.sh-Datei ausgeführt werden, z. B. durch "bash run.sh".- Sollen nur bestimmte Tests durchlaufen werden, müssen die entsprechenden Tags in der run.sh gesetzt werden.
- Die Option "--format=pretty" sorgt dafür, dass neben den einzelnen Testschritten auch jeweils die zugehörigen php-Funktionen inklusive Scope ausgegeben werden, was beim Debuggen hilfreich sein kann.
Soll die Ausgabe des Test-Durchlaufs zusätzlich in einer Datei gespeichert werden, empfiehlt sich folgender Befehl:
- bash run.sh > output.txt
Faildump
Wenn ein einzelner Schritt fehlschlägt bzw. nicht durchgeführt werden kann, werden alle weiteren Schritte des Szenarios übersprungen und das Szenario gilt als fehlgeschlagen. Falls ein Szenario (oder mehrere) fehlschlägt, wird unter http://localhost:8000/_/faildumps/ ein entsprechendes Verzeichnis erstellt, das eine .html-Datei mit dem Zustand von Moodle zum Zeitpunkt des fehlgeschlagenen Schrittes enthält. Ist das Szenario mit einem @Javascript-Tag annotiert, wird zusätzlich auch ein Screenshot von Moodle gemacht und ebenfalls in dem Verzeichnis abgelegt. -
Behat-Tests liegen immer im Verzeichnes /tests/behat/ des Plugins. Unterschiedliche Tests sollten in eigenen Dateien mit der Endung *.feature liegen.
-
Wie werden die einzelnen Schritte eines Tests ausgeführt?
Die einzelnen Testschritte sind jeweils einer PHP-Funktion zugeordnet. Diese Funktionen können schlicht Änderungen an der Moodle-Instanz vornehmen (z. B. Admin-Settings setzen) oder aber auf Verhalten prüfen und bei Fehlschlag eine "Exception" werfen.
Die Zuordnung "Behat-Testschritt → PHP-Funktion" erfolgt über eine Kommentar-Annotation der entsprechenden PHP-Funktion. Werden die Behat-Tests mit der Flag --format=pretty ausgeführt, wird zu jedem durchgeführten Schritt die zugehörige PHP-Funktion angegeben.
Beispiel:
And I log in as "admin" # behat_auth::i_log_in_as()
Im Scope "behat_auth" befindet sich also eine Funktion "i_log_in_as()". Wird in einem Moodle-Verzeichnis nach "behat_auth" gesucht, so findet sich die Datei "auth\tests\behat\behat_auth.php". In dieser Datei befindet sich die Klasse behat_auth, die von behat_base erbt, und innerhalb der Klasse die entsprechende Funktion i_log_in_as().
auth/tests/behat/behat_auth.php
<?php [...] require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); [...] class behat_auth extends behat_base { /** * Logs in the user. There should exist a user with the same value as username and password. * * @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)"$/ * @Given I am logged in as :username * @param string $username the user to log in as. * @param moodle_url|null $wantsurl optional, URL to go to after logging in. */ public function i_log_in_as(string $username, moodle_url $wantsurl = null) { // In the mobile app the required tasks are different (does not support $wantsurl). if ($this->is_in_app()) { $this->execute('behat_app::login', [$username]); return; } $loginurl = new moodle_url('/auth/tests/behat/login.php', [ 'username' => $username, ]); if ($wantsurl !== null) { $loginurl->param('wantsurl', $wantsurl->out_as_local_url()); } // Visit login page. $this->execute('behat_general::i_visit', [$loginurl]); } [...] }
Der PHPDoc-Kommentar oberhalb der Funktion enthält eine kurze Beschreibung und anschließend zwei Zeilen, die jeweils mit @Given
beginnen und dem dann ein regulärer Ausdruck folgt. Diese beiden Zeilen verwendet Behat, um beim Schreiben eines Test-Szenarios die einzelnen Schritte der zugehörigen PHP-Funktion zuzuweisen und die Argumente entsprechend zu übergeben.So weit beurteilbar, muss es sich bei dem Wort unmittelbar nach dem @ um eines der drei Wörter eines Behat-Tests handeln: Given
,When
,Then
. Unabhängig davon, welches Wort hier gewählt wird, kann dieser Testschritt auch mit den anderen Wörtern, insbesondere auch mitAnd
undBut
, aufgerufen werden.Der darauffolgende Ausdruck gibt dann den Text wieder, der in dem entsprechenden Test-Szenario geschrieben werden muss, um diese Funktion aufzurufen. Der Ausdruck kann dabei sogenannte "named capture groups" beinhalten, die es einem ermöglichen, Text aus dem regulären Ausdruck zu extrahieren und in die in spitzen Klammern stehende Variable zu schreiben. So können die im Behat-Test beschriebenen Werte als Parameter der PHP-Funktion übergeben werden.Innerhalb der Funktion kann anschließend normales PHP verwendet werden. Handelt es sich bei dem Testschritt um einen "
@Then
"-Schritt, sollte die Funktion auf entsprechendes Verhalten prüfen und bei Fehlschlag eine entsprechende Exception werfen bspw.ExpectationException
,ElementNotFoundException
,DriverException
. Die Exceptions befinden sich jeweils inBehat\Mink\Exception\
. DieDriverException
entsteht in der Regel dadurch, dass versucht wird, eine JavaScript-Funktion auszuführen, obwohl der Test nicht entsprechend annotiert wurde (und deswegen die JavaScript-Engine des Browsers nicht zur Verfügung steht).
JavaScript kann in der PHP-Funktion wie folgt ausgeführt werden:
if (!$this->running_javascript()) { throw new DriverException("JavaScript-Engine is not running."); } $jsvariable = "
<javascript-code-snippet>
"; $result = $this->evaluate_script($jsvariable); Sollte der JavaScript-Code einen Rückgabewert haben, wird dieser in $result
gespeichert und kann anschließend ausgewertet werden.
Neben der semantischen und syntaktischen Richtigkeit der Funktion gibt es auch noch Good-Practice-Regeln im PHP-Kontext zu beachten:
- Die Funktion sollte exakt einen Schritt eines Behat-Test-Szenarios abdecken. Falls für das Szenario mehrere Schritte notwendig sind und die jeweiligen Funktionen nicht schon existieren, sollten entsprechend mehrere Funktionen geschrieben werden. Kleine atomare Schritte machen es beim Fehlschlagen des Tests einfacher nachzuvollziehen, was genau schiefgelaufen ist.
- Die
Stolpersteine eines Test-Schrittes sollten vorab durchdacht werden und
die Funktionentsprechende Fehlermeldungen werfen.
Beispielsweise:
- Wird Javascript zur Durchführung benötigt? → Prüfe, ob die Javascript-Engine ansprechbar ist, sonst
DriverException
. - Soll ein bestimmtes Element auf der Website stehen? Prüfe, ob es auch tatsächlich existiert, sonst
ElementNotFoundException
. - Soll ein Element einen bestimmten Wert enthalten? Falls dieser Wert nicht vorliegt
ExpectationException
werfen.
- Wird Javascript zur Durchführung benötigt? → Prüfe, ob die Javascript-Engine ansprechbar ist, sonst
- Exceptions sollten aussagekräftige Mitteilungen mitgegeben werden, damit sich ein Bug schneller finden lässt. Bei einer
ExpectationException
empfiehlt es sich bspw. erwartete und tatsächliche Werte auszugeben.
-
- Nur genau eine Sache pro Szenario testen!
- Im besten Fall sollte jedes Szenario immer exakt einen einzigen Teil der Funktionalität testen. Daher empfiehlt es sich, dass die Szenarien entsprechend sinnvoll benannt sind, so dass im Falle eine Testfehlschlags der Name ein detaillierter Hinweis auf den Bug ist.
- Das Set-Up (Given) des Szenarios sollte nicht die Benutzeroberfläche verwenden!
- Die Schritte unter Given sollten die Test-Situation herstellen, ohne mit dem UI zu interagieren. Statt bspw. einen Kurs durch das Klicken der jeweiligen Buttons zu erzeugen, sollte [The following "courses" exist] verwendet werden, um die Daten direkt in der Datenbank zu hinterlegen. Falls nötig empfiehlt es sich, zusätzliche Schritte speziell für das zu testende Plugin zu schreiben.
- Statt zu einer bestimmten Seite zu navigieren, kann [Given I am on the "C1" "Course" page] verwendet werden. Die Navigation von Moodle hat sich in der Vergangenheit schon verändert und so wird umgangen, dass nach dem Einloggen zunächst das Dashboard als Landungsseite fungiert, welches sehr langsam lädt.
- Keine XPath- oder CSS-Selektoren verwenden, sondern Accessibility Bugs beheben!
- Falls es nur möglich ist, ein Element, das manipuliert werden soll, mit einem Schritt wie bspw. [I set the field with xpath "//textarea[contains(@name, 'answer')]" to "frog"] zu manipulieren, spricht dies in der Regel für einen Accessibility Bug.
- Behat sieht die Seite ziemlich genau so, wie der Benutzer eines Screenreaders. Es sollte möglich sein, das Element mit [I set the field "Answer" to "frog"] oder [I click on "True" "radio" in the "First question" "question"] anzusprechen. Falls nicht, sollte der entsprechende Accessibility Bug behoben werden, statt einen unleserlichen Behat-Test zu schreiben.
- Es sollte ersichtlich sein, zu welchem Plugin neu geschriebene Schritte gehören!
- Beim Schreiben eigener Schritte sollte darauf geachtet werden, dass sie nicht zu generisch sind und die Zugehörigkeit zum Plugin ersichtlich ist. Statt [I disable UI plugins] wäre bspw. [I disable UI plugins in the myplugin settings] möglich.
- Nur genau eine Sache pro Szenario testen!