Integration Tests mit Gürkchen – Teil 3

In dieser Artikelserie geht es um die lesbare Beschreibung von Integration-Tests mit Gherkin. Im ersten und zweiten Teil haben wir zwei Test-Szenarios geschrieben. In diesen Szenarios waren Fixtures (Testdaten) enthalten, gegen die anschließend getestet wurde. In diesem Teil statten wir das Ganze noch mit Dependency Injection aus, damit wir das eklige Singleton wieder entfernen können.


Leider ist die Dokumentation von Behat sehr Rookielastig – Basics werden gut erklärt, aber über tiefergehende Features gibt es wenig bis keine Anleitung oder Blogposts.

Somit bin ich erst dem Tipp eines Kollegen (@Ando) gefolgt. Behat bietet die Möglichkeit, mittels eines ContextInitializer die Kontexte vor Benutzung nochmal vorzubereiten. Dieser Initializer muss mit einer Extension angelegt und über die Konfiguration verknüpft werden.

Mittels dieses Konstruktes ist es möglich, einen beliebigen DI-Container (Zend, Symfony, Laravel, oder was auch immer) zu erzeugen und über den ContextInitializer an die Kontexte übergeben.

Es würde also nach Erzeugung des FeatureContext (und somit bevor es im Test benutzt wird) die Methode setDIContainer aufgerufen, die dem Context den DI-Container übergibt, und aus dem man sich dann z.B. den JiraIssueContainer holen kann.

Das funktioniert, ist aber viel Code, und vor allem bekommt der Kontext statt der benötigten Abhängigkeiten den DI-Container. Das ist aus mehreren guten Gründen ein Konstrukt, von dem man ja inzwischen auch in Zend abgekommen ist. Anstatt den kompletten Container zu übergeben sollte eine Klasse nur genau die Abhängigkeiten bekommen, die sie benötigt.

Beim Debuggen durch Behat hatte ich bereits gesehen, dass Kontexten Argumente übergeben werden können, und bei FriendsOfBehat gibt es auch eine Extension, die eine externe Container-Spezifikation im Symfony-Format laden kann. Allerdings erlaubt diese nur das Laden von Parametern, nicht von Services, und bei meinen Versuchen das nachzubauen kam ich auch nicht weiter.

Der Groschen fiel, als ich über diesen Code in der ServicesResolverFactory gestolpert bin:

public function createArgumentResolvers(Environment $environment)
    {
        $suite = $environment->getSuite();

        if (!$suite->hasSetting('services')) {
            return array();
        }

        $container = $this->createContainer($suite->getSetting('services'));
        $autowire = $suite->hasSetting('autowire') && $suite->getSetting('autowire');

        if ($environment instanceof ServiceContainerEnvironment) {
            $environment->setServiceContainer($container);
        }

        return $this->createResolvers($container, $autowire);
    }

Ah, man kann der Suite also irgendwas unter services anhängen? Das erzeugt dann wohl einen ServiceContainer…

Das Ergebnis: Behat erlaubt es unterhalb der Suite einen im Symfony-Format definierten Container zu definieren:

default:
  autoload:
    'Test': src/
  suites:
    default:
      path: features
      contexts:
        - \Test\Context\InitializerContext:
          - "../../../config/"
        - \Test\Context\JiraFeatureContext:
          - "@test.fixture.jira"
        - \Test\Context\CommandFeatureContext:
          - "@test.fixture.jira"
          - "@test.fixture.bitbucket"
        - \Test\Context\BitbucketFeatureContext:
          - "@test.fixture.bitbucket"

      services:
        test.fixture.jira:
          class: \Test\Fixture\JiraIssueContainer

        test.fixture.bitbucket:
          class: \Test\Fixture\BitbucketPullRequestContainer

Hier kann ich also unter “services:” meine Klassen definieren und mit einer ID (z.B. test.fixtures.jira) versehen. Und in den Kontexten kann ich mittels “@test.fixtures.jira” dann einfach diese Abhängigkeiten injizieren. Der JiraFeatureContext hat also danach einen Konstruktor:

/**
     * JiraFeatureContext constructor
     *
     * @param JiraIssueContainer $jiraIssueContainer A container to collect the fixture issues in
     *
     * @return void
     */
    public function __construct(JiraIssueContainer $jiraIssueContainer)
    {
        parent::__construct();
        $this->jiraIssueContainer = $jiraIssueContainer;
    }

Es kann so einfach gehen! Also weg mit dem Singleton-Code, jetzt könnte ich sogar Tests für die Test-Objekte schreiben!

Aufräumen

Außerdem war bisher unschön, dass in Gherkin auf den Namen des customfield verwiesen wurde. Das ist Konfiguration, und gehört genau dort hin. Somit habe ich den Sentence umbenannt:

Then I expect the issue "PVKZU-123" to have the field "customfield_11600" with the content "---" 
   wird nun zu
Then I expect the issue "PVKZU-123" to have the PRState "---"

Dazu muss ich die Konfiguration der Applikation laden. Diese liegt natürlich nicht im Test-Verzeichnis, und somit muss ich den Pfad zur Anwendungs-Konfiguration ebenfalls einstellen. Da das thematisch in keinen der bestehenden Kontexte passt, lege ich einen neuen Kontext “InitializerContext” an, dem ich in der behat.yml den relativen Pfad zur Config übergebe.

contexts:
        - \Test\Context\InitializerContext:
          - "../../../config/"

Das wird ebenso in den Konstruktor übergeben, und dort wird die Konfiguration direkt geladen. Da DotEnv anders als bei Zend kein Config-Objekt nutzt, das durchgereicht werden muss, können wir die Abhängigkeiten klein halten. DotEnv wird einfach in Environment-Variablen übersetzt, die man mittels der PHP-Funktion getenv($key) abrufen kann. Architektonisch ist das ein wenig fragwürdig, aber in diesem Fall zweifellos praktisch.

Fazit

Durch diese Erweiterung ist das eine schöne runde Sache, wir werden in Zukunft wesentliche Teile unserer Integrationtests in Gherkin verfassen, und auch bestehende, schwer zu lesende und verstehende Tests umstellen. Die Tests können jetzt auch vom Produktmanagement abgenommen werden! Und da Entwickler bekanntermaßen schlechte Tester sind, ist allein das schon ein Grund, warum diese Schreibweise ein riesiger Fortschritt ist.

Wie immer: Feedback hierzu interessiert mich sehr, einfach unten kommentieren oder mich per Mail oder persönlich ansprechen!

About the author

People Enabler at CHECK24