Nikolas Martens hat am Wochenende auf der FrOSCon in seinem Vortrag „The Pyramid is a lie“ vorgestellt wie Tests richtig laufen sollten. Ich möchte nun hier nochmal die Erkenntnisse aus dem Talk darstellen. Die Slides von Nikolas findet man hier.
Wer kennt sie nicht – Die Pyramide. In beinahe jedem Testing-Worshop oder -Votrag kommt sie vor. Decke beinahe den ganzen Code mit vielen, günstige und schnellen Unit-Tests ab, jedoch nur wenige mit langsamen UI-Tests. Dazwischen käme eigentlich noch der Teil der Service-Tests, wird aber meistens vernachlässigt. Hält man sich die Vor- und Nachteile von Unit-Test und UI-Tests vor Augen so wird man schnell feststellen, dass diese beinahe komplementär sind. Der Nachteil des Einen ist der Vorteil des Anderen. Die genaue Abbildung von Akzeptanz-Kriterien in UI-Tests wird zum Nachteil, wenn man viele solcher Tests laufen lässt. Viele laufen lassen, kann man bei Unit-Tests, doch meist deckt man damit jeweils immer einen viel zu kleinen Teil ab. Diese Tests lassen sich so dann selten an das Business kommunizieren.
Wie Nikolas am Wochenende ging auch schon Everzet in seinem Blog-Post darauf ein, dass wir mit der gewohnten Pyramide meist den wichtigsten Teil der Applikation vergessen:
Domain Core – Service-Layer
Diese beinhaltet die eigentliche Business-Logik, bildet also auch das Domain-Model ab. Wir können zwar dessen Komponenten mit Unit-Test abdecken, doch die komplette Funktionalität lässt sich nur mit „etwas größeren“ Tests sicher stellen.
Bedenkt man noch, dass durch die featureweise Entwicklung viele Schnittmengen zwischen verschiedenen Features und den damit verbundenen Tests entstehen, führt das einen schnell zu dem Schluss: „Warum die Tests nicht im Service Layer beginnen?“. Dabei kann man die Tests dann gleich in einer business-verständlichen Form umsetzen – der sogenannten Ubiquitous Language.
Business Need: Edit users In order to have customer support an Admin want's to edit a user. Scenario: Edit user data on behalf of a customer Given a user with email "maximilian.berghoff@mayflower.de" When i change the username to "ElectricMaxxx" Then the user profile should display the username "ElectricMaxxx"
Das Szenario ist in der sogenannten Gherkin Notation geschrieben und wird hier mit Behat umgesetzt. Das heißt man schreibt für deren Ausführung ein Kontext-Objekt:
user = $this->repository->getUserByEmail($email); } /** * @Then i change the username to :username */ public function iChangeTheUserNameTo($userName) { $this->user->setUserName($userName); $this->repository->persist($this->user); } /** * @Then the user profile should display the username :userName */ public function theUserProfileShouldDisplayTheUsername($userName) { $userProfile = $this->repository->getUserByEmail($this->user->getEmail); $this->assertEquals($userProfile->getUserName, $userName); } }
Woher jetzt das Repository und die Asserts kommen sei mal dahin gestellt. Beispielsweise könnte das Repository hier einfach nur ein LocalCache oder ein Mock sein.
Was wäre wenn wir jetzt den selben Test für das UI-Testing benutzen würden? Dann würden wir einfach ein anderes Kontext-Objekt erstellen:
getUserIdByEmail($email); $this->visit('/admin/user/'.$id); $this->userInput = $this->find('css', 'input["name=\'user\']'); } /** * @Then i change the username to :username */ public function iChangeTheUserNameTo($userName) { $this->userInput->insert($userName); $this->find('css', 'button[name=\'submit\']')->click(); } /** * @Then the user profile should display the username :userName */ public function theUserProfileShouldDisplayTheUsername($userName) { $this->visit('/profile'); $userNameDiv = $this->find('css', 'div#user:contains(\''.$userName.'\''); $this->assertTrue($userNameDiv); } }
Die definierten Steps des Szenarios sind die selben nur die Implementierung unterscheidet sich. Die Methoden zum finden von Seiten und HTML-Element kann beispielsweise durch den Symfony-Crawler oder aber auch von Selenium übernommen werden, soll hier aber nicht das Thema sein.
Wir haben jetzt aber mit der selben Test-Syntax zwei verschiedene Test-Implementierungen geschaffen. Vor allem entspricht diese Test-Syntax der Ubiquitous Language und sollte sowohl von Business verstanden werden als auch beispielsweise Akzeptanz-Kriterien der User-Story abdecken. Wie kann ich diese Tests je nach Context laufen lassen? Behat liefert uns dafür eine Konfigurationsmöglichkeit:
default: suites: domain: contexts: [ UserContext ] ui: contexts: [ WebUserContext ]
Damit kann man Tests nun für das Domain-Model und für das Userinterface separat oder gleichzeitig laufen lassen. Das Szenario ist nur einmal geschrieben, den Context kann ich aber n-mal implementieren. So könnte auch eine Implementierung zusätzlich die den PATCH von User Daten über die API abdecken, die Konsequenz sollte die selbe sein.
Schreibe einen Kommentar