Integration Tests mit Gürkchen – Teil 1

Einleitung

Wer schon öfter Integration Tests mit PhpUnit oder anderen ähnlichen Frameworks geschrieben hat, der weiss, dass das leicht ausartet. Vor allem, wenn man alten Code testet, der von Dependency Injection noch nie etwas gehört hat.

Da wird dann über 50 oder 100 oder mehr Zeilen gemockt, prophezeit, expected und asserted, und am Ende weiß man eine Woche später schon gar nicht mehr, was man da überhaupt testet.

Wenn man das nur einfacher und vor allem lesbarer lösen könnte! Es wäre doch toll, wenn man einfach sowas schreiben könnte:

Angenommen, ich habe Wasser und Mehl. Wenn ich beides vermische, dann sollte ich Teig bekommen

Jetzt übersetze ich das schnell und formatiere ein wenig:

Given I have water and flour
When I mix them together
Then I should have dough

Spätestens jetzt erkennt jeder, der schon einmal BDD gemacht hat, und dem Behat ein Begriff ist, dass man so Tests schreiben kann. Diese Sprachweise mit Given, When, Then kommt von Ruby und heisst dort Cucumber (Gurke). Die PHP-Variante ist nicht so selbstsicher, und nennt sich zaghaft Essiggürkchen (Gherkin). Obwohl eigentlich zum Test aller möglicher Dinge geeignet, ist das bei PHP mit Behat stark auf das Testen von Weboberflächen ausgelegt.

Aber mit ein wenig Tricksen klappt es auch mit PhpUnit und Prophecy. Wie das geht, das werde ich in diesem und den folgenden Artikeln erklären.

Beispielprojekt

Ausprobieren wollen wir das Ganze an einem unserer Infrastrukturprojekte, weil das immer spannender ist, als an einem erdachten Dummy-Beispiel.

Wir nutzen die Atlassian-Tools, und uns fehlt in Jira die Information über die PullRequests: Welche gibt es? Wo ist noch Nacharbeit nötig? Wer hat da Review noch nicht durchgeführt?

Dafür haben wir uns eine Lösung gebastelt, die regelmäßig die PullRequests durchgeht und die Informationen in die Tickets vermerkt. Diesen Code wollen wir mit Integration-Tests abdecken.

Ticketansicht

Das Feld ist ein Freitext-customfield, dass wir in Jira für unser Project angelegt haben, und das wir in die Ticketansicht konfiguriert haben. Ein über systemd minütlich ausgeführtes Command aktualisiert die Informationen in den Tickets.

Was bedeuten die Informationen dort? D und M stehen für unser Desktop Frontend und das Mobile Frontend. Der PullRequest sowohl für Desktop als auch für Mobile ist abgenommen, denn es ist ein Häkchen dahinter. Es werden alle Pull-Requests zu dem Ticket aufgelistet, und danach jeweils der Status (Keine Reviewer, Abgelehnt, Noch fehlende Reviews, Abgenommen, Gemergt). Im Status Abgelehnt und Fehlende Reviews werden zusätzlich noch weitere Informationen hinten angefügt, die dann in der Regel aus Platzgründen nur im Tooltip zu sehen sind: Wer fehlt noch, wer hat abgelehnt. Dazu kommen wir aber später.

Was genau wollen wir erreichen?

Wir wollen keinen End-To-End-Test schreiben, der Jira, Bitbucket, Netzwerk, etc. mit abtestet. Es soll nur das Command “PullRequestToJira” getestet werden. Dazu müssen alle externen Abhängigkeiten gemockt werden, und nur unsere Testdaten sollen benutzt werden.

Unser erster Testfall

Wir testen zunächst den einfachsten Testfall:

Feature: Make sure that the status of a pull request is matched in the jira issue

  Scenario: Mock a ticket without a pullrequest and assert that the ticket will have the correct label
    Given I have an issue "PVKZU-123" with the summary "Change background color of all CTA Buttons to unicorn pink"

    When I execute the PullRequestToJira command
    Then I expect the issue "PVKZU-123" to have the field "customfield_11600" with the content "---"

Also: Ticket ohne PullRequest, Command laufen lassen, in unserem CustomField steht dann “—” als Hinweis darauf, dass es eben noch keinen PR gibt.

Umsetzung

Als erstes benötigen wir einmal einen Ordner “test” im Projekt für die neuen Tests. Dort Unterordner für Unit- und Integrationtests. In dem test/integration-Ordner kommt ein Ordner “features”, in dem wir die in Gherkin geschriebenen Testrezepte ablegen, sowie ein src-Ordner, in denen die Feature-Kontexte und weiterer Code liegt:

Ordnerstruktur

Als Nächstes müssen wir uns die benötigten Bibliotheken per composer installieren:


> composer require-dev phpunit/phpunit
> composer require-dev behat/behat

Außerdem ist es generell bei der Arbeit mit Unittests sinnvoll, auch in PhpStorm eine Erweiterung zu installieren: “PHPUnit Enhancement”. Darin enthalten ist auch ein wenig Support für Prophecy, der allerdings noch ausbaufähig ist.

Behat Config

Nun benötigen wir eine Konfigurationsdatei für Behat, die unter test/integration/behat.yml abgelegt wird:

default:
  autoload:
    '': src/
  suites:
    default:
      path: features
      contexts:
        - \Contexts\JiraFeatureContext

Also: Wir legen eine Default-Konfiguration an, die den Composer-Autoloader nutzt und den Root-Namespace auf src/ setzt. Dann konfigurieren wir die Default-Suite so, dass er die Testrezepte aus dem Pfad “features” liest, und den FeatureContext \Contexts\JiraFeatureContext benutzt.

Diesen legen wir nun an:

Man beachte, dass ich src gleich als “Sources Root” definiert habe (Rechte Maustaste, Mark Directory As …). Das hilft PhpStorm die Codestruktur zu verstehen, und beim Anlegen von Klassen z.B. direkt den korrekten Namespace einzutragen.

Die Klasse JiraFeatureContext muss das leere Interface Behat\Behat\Context\Context (sic!) implementieren, um als FeatureContext erkannt zu werden. Hier werden die Implementierungen aller Sentences liegen, die Jira betreffen.
Außerdem legen wir nun unter features die oben beschriebene Gherkin-Datei als prs2jira.feature an.

Wir wollen nun den ersten Sentence Given I have an issue “<Issue-Key>” with the summary “<Issue-Summary>” implementieren:

  /**
     * Create a new issue mock
     *
     * @Given /^I have an issue "([^"]*)" with the summary "([^"]*)"$/
     * @param string $issueKey     The key for the issue
     * @param string $issueSummary The summary for the issue
     *
     * @return void
     */
  public function 
    iHaveAnIssueWithTheSummary(string $issueKey, string $issueSummary): void
  {
  }

Was soll nun passieren? Wir wollen, dass das Command später nicht Jira anfragt, sondern die Anfragen auf ein Mock-Objekt umgeleitet werden. Das soll von uns definierte Daten zurückgeben. Also merken wir uns erst mal einfach diese Daten in einem Array. Um das Mocking später zu vereinfachen, speichern wir die Daten genau so, wie sie die Jira-API auch zurückgibt, also in einem Objekt, das eine Eigenschaft total und ein Array issues enthält.

Kleiner Stolperstein: Wir haben keinen DI-Container zur Verfügung, und somit müssen wir Patterns aus dem Grab holen, die eigentlich dort liegen bleiben sollten. Mittels Singleton-Pattern basteln wir eine Container-Klasse, in der wir die Issues ablegen können. Dazu ein Getter für $issues, und fertig ist unsere Datenspeicherung.

/**
     * JiraIssueContainer constructor
     */
    private function __construct()
    {
        $this->issues         = new \stdClass();
        $this->issues->issues = [];
        $this->issues->total  = 0;
    }

Nächste Woche wagen wir den nächsten Schritt, und führen mal das PullRequestsToJira-Command mit unserem gemockten Jira-Server aus.

About the author

People Enabler at CHECK24

Comments

  1. Bezüglich fehlendem DI-Container habe ich dank meines schlauen Lead-Developers eine Lösung gefunden, das wird dann wohl Teil 3 der Serie werden.

Comments are closed.