Kursthemen

  • Starthilfe

    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:

  • Quickstart


    Docker sollte installiert sein. Falls unter Windows gearbeitet wird zusätzlich auch WSL2.




    • "Docker Containers for Moodle Developers" klonen

      Für das einfache Aufsetzen einer Test-Umgebung stellt Moodle selber eine Docker-Konfiguration bereit:


      Moodle klonen

      Die Test-Umgebung kommt ohne eine eigene Moodle-Instanz, diese wird zusätzlich benötigt.


      Plugin mit neuem Feature klonen

      Innerhalb des Moodle-Verzeichnisses kann dann das Plugin mit dem zu testenden Feature eingebundenwerden, bspw. als Submodule.


      run.sh erstellen

      Anschließend muss eine Datei run.sh erstellt werden.

      • Die Datei run.sh sollte im Verzeichnis "moodle-docker" liegen.

        run.sh

      • 1 # Set up path to Moodle code 2 export MOODLE_DOCKER_WWWROOT="./moodle-401" 3 # Choose a db server (Currently supported: pgsql, mariadb, mysql, mssql, oracle) 4 export MOODLE_DOCKER_DB=pgsql 5 6 # Ensure customized config.php for the Docker containers is in place 7 cp config.docker-template.php $MOODLE_DOCKER_WWWROOT/config.php 8 9 # Start up containers 10 bin/moodle-docker-compose up -d 11 12 # Wait for DB to come up (important for oracle/mssql) 13 bin/moodle-docker-wait-for-db 14 15 # Work with the containers (see below) 16 # Initialize behat environment 17 bin/moodle-docker-compose exec webserver php admin/tool/behat/cli/init.php 18 19 # Run behat tests 20 bin/moodle-docker-compose exec -u www-data webserver php admin/tool/behat/cli/run.php --tags="@plugin_feature_tag" --format=pretty --out=std 21 22 # Shut down and destroy containers 23 bin/moodle-docker-compose down
        24

      • Die Variable MOODLE_DOCKER_WWWROOT muss dabei auf das Verzeichnis zeigen, in dem Moodle liegt.
      • In Zeile 20 müssen die Tags für den jeweiligen Test eingesetzt werden.
      • Die letzte Zeile kann auskommentiert werden. Dadurch bleibt der Container auch nach dem Ausführen der Tests bestehen. Beim erneuten Testen muss so nicht erneut eine komplette Moodle-Instanz aufgebaut werden.
  • Tests ausführen

    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.
  • Eigene Testfälle schreiben

    Behat-Tests liegen immer im Verzeichnes /tests/behat/ des Plugins. Unterschiedliche Tests sollten in eigenen Dateien mit der Endung *.feature liegen.

    • Aufbau einer feature-Datei

      Die @-Tags am Anfang können dazu verwendet werden, um beim tatsächlichen Testen nur eine spezielle Auswahl der Testfälle zu berücksichtigen. Der entsprechende Tag wird dafür in Zeile 20 der run.sh-Datei eingesetzt (siehe oben).

      Einzelnen Test-Szenarien können zusätzlich mit einem Javascript-Tag annotiert werden, falls enthaltene Schritte Javascript zur Ausführung benötigen. Generell sind Javascript-Tests jedoch wesentlich langsamer als reguläre Tests. Sollten alle Szenarien Javascript benötigen, reicht es aus, den Tag am Anfang der Datei zusammen mit den anderen Tags zu setzen.


      Tests sollten im Allgemeinen folgenden Aufbau haben:

      1. Given: Diese Schritte werden dazu verwendet, die Test-Situation bereitzustellen. Üblicherweise werden hier Benutzer, Kurse oder Plugin-Instanzen erstellt. Der Given-Teil soll noch nichts testen, deswegen sollte er kurz und verlässlich sein.
      2. When: Hier werden die Aktionen beschrieben, die die Test-Umgebung zu dem Punkt führen, an dem tatsächlich Bedingungen geprüft werden sollen. Dies kann bspw. das Einloggen, Navigieren zwischen Seiten, das Einreichen oder das Bewerten von Aufgaben beinhalten.
      3. Then: In diesen Schritten soll überprüft werden, ob sich das Plugin tatsächlich so verhält wie erwartet. Hier geschehen in der Regel recht simple Abfragen, z. B. ob ein Text oder ein bestimmtes Element sichtbar bzw. nicht sichtbar ist.

      Ein konkreter Testfall sollte jedes dieser drei Worte nur einmal und in genau dieser Reihenfolge enthalten. Falls Given, When oder Then mehrere Schritte enthalten, können dafür die Wörter And und But verwendet werden.

      
      Given the following user exists:
        | username   | ccolon             |
        | First name | Colin              |
        | Last name  | Colon              |
        | email      | ccolon@example.com |
      And the following course exists:
        | Name      | Jump Judging (Level 1) |
        | Shortname | sjea1                  |
      When I log in as "ccolon"
      And I navigate to "Site home > Jump Judging (Level 1)"
      Then I should see "You are not enrolled in this course"
      But I should see "Enrol now"
      
      Sollten mehreren Szenarien dasselbe Set-Up benötigen, können die entsprechenden Given-Schritte als Background zusammengefasst werden.
      
      Background:
          Given the following "courses" exist:
            | fullname | shortname | category | groupmode |
            | Course 1 | C1        | 0        | 1         |
          And the following "users" exist:
            | username | firstname | lastname | email |
            | teacher1 | Theo | Teacher | teacher1@example.com |
      
      
      Diese Schritte werden dann vor jedem folgenden Szenario ausgeführt. In den einzelnen Szenarien können noch weitere, individuelle Given-Schritte definiert werden.
    • Beispiel für feature-Datei

      moodle/blocks/tests/behat/restrict_available_blocks.feature
      
      @core @core_block
      Feature: Allowed blocks controls
        In order to prevent the use of some blocks
        As an admin
        I need to restrict some blocks to be used in courses
       
        Background:
          Given the following "users" exist:
            | username | firstname | lastname | email |
            | teacher1 | Teacher | 1 | teacher1@example.com |
          And the following "courses" exist:
            | fullname | shortname | category |
            | Course 1 | C1 | 0 |
          And the following "course enrolments" exist:
            | user | course | role |
            | teacher1 | C1 | editingteacher |
       
        Scenario: Blocks can be added with the default permissions
          Given I log in as "teacher1"
          And I am on "Course 1" course homepage with editing mode on
          When I add the "Course completion status" block
          And I add the "Activities" block
          Then I should see "Activities" in the "Activities" "block"
          And I should see "Course completion status" in the "Course completion status" "block"
       
        Scenario: Blocks can not be added when the admin restricts the permissions
          Given I log in as "admin"
          And I set the following system permissions of "Teacher" role:
            | block/activity_modules:addinstance | Prohibit |
          And I am on the "Course 1" "permissions" page
          And I override the system permissions of "Teacher" role with:
            | block/completionstatus:addinstance | Prohibit |
          And I log out
          When I log in as "teacher1"
          And I am on "Course 1" course homepage with editing mode on
          Then the add block selector should not contain "Activities" block
          And the add block selector should not contain "Course completion status" block
      
      
    • Liste aller zur Verfügung stehender Schritte

      Wenn die in der run.sh-Datei enthaltenen Schritte bis inklusive Zeile 17 (Initialize behat environment) nacheinander im Terminal ausgeführt werden, kann die Test-Instanz anschließend auch im Browser unter localhost:8000 aufgerufen werden. Unter "Site administration > Development > Acceptance Testing" findet sich dort eine vollständige Liste aller Test-Schritte, die für die Moodle-Instanz zu Verfügung stehen, inklusive der Schritte installierter Plugins. Die Funktionsnamen und Beschreibungen werden dabei aus den PHPDoc-Kommentaren der jeweiligen Funktionen generiert.

  • Eigene Schritte schreiben



    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 mit And und But, 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. ExpectationExceptionElementNotFoundExceptionDriverException. Die Exceptions befinden sich jeweils in Behat\Mink\Exception\. Die DriverException 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.
    • 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.
  • Good Practices



    • 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.