© istockphoto.com/Varijanta
Funktionierende Software trotz Refaktorisierungen

Funktionale Tests im Refactoring


Wie schon oft diskutiert ist es nahezu nie sinnvoll, Software neu zu schreiben. Exis­tierende Software sollte, insbesondere wenn niemand mehr genau versteht, was sie macht, refaktorisiert werden [1], [2]. Refactoring bezeichnet die Technik, die internen Strukturen von existierendem Code zu ändern, ohne dessen Funktionalität zu beeinträchtigen. Wie kann das, gerade bei größeren Applikationen, erreicht werden?

Kurz zusammengefasst erhöht sich bei einem Rewrite nur in den seltensten Fällen tatsächlich die Softwarequalität, es gehen Features verloren, denen sich keiner der Stakeholder mehr bewusst war (üblicherweise etwa 40 Prozent) und die Umsetzung dauert immer deutlich länger als antizipiert. Außerdem können oft während eines Rewrites keine neuen Features mehr implementiert werden, wenn man nicht ein neues Team schafft, dass dann entsprechend weniger Wissen über die existierende Softwarefunktionalität hat.

Die einfachste Variante, um sicherzustellen, dass Funktionalität erhalten bleibt, sind automatisierte Tests. Automatisierte Tests für Software werden jedoch oftmals mit Unit-Tests gleichgesetzt, und genau diese lassen sich im Normalfall nicht für alte Software schreiben. Es ist immer noch üblich, dass exzessiv statische Methodenaufrufe stattfinden und Singletons oder statische Registries verwendet werden. Diese und andere veralteten Strukturen erschweren das Schreiben von Unit-Tests deutlich und sind nicht zuletzt oft entscheidende Gründe, warum ein Refactoring in Angriff genommen werden soll. Ein Teufelskreis.

Der Ablauf

Automatisierte Tests müssen nicht immer Unit-Tests sein. Andere Testformen wie Integrationstests, Akzeptanztests oder funktionale Tests haben ebenfalls ihre Daseinsberechtigung. Und genau die funktionalen Tests können uns beim Refactoring helfen. Funktionale Tests haben die Aufgabe sicherzustellen, dass Software eine gewisse Funktion oder Aufgabe erfüllt. Dabei wird bei einer Webapplikation zum Beispiel getestet, dass der Check-out in einem Onlineshop funktioniert, in dem dieser komplett ausgeführt wird. Die Idee ist Folgende:

  • Ein Anwendungsfall der Software wird durch einen funktionalen Test beschrieben.

  • Wir stellen sicher, dass der zu refaktorisierende Code durch die Tests abgedeckt wird.

  • Der Code kann refaktorisiert werden.

  • Wir können Unit-Tests für wichtigen neuen Code schreiben.

Durch den angedeuteten Weg ist sichergestellt, dass zumindest der getestete Anwendungsfall weiter funktioniert. Der Code kann mit den hier später beschriebenen Techniken angepasst und aufgeräumt werden. Für den neuen sauberen Code können Unit-Tests geschrieben werden – dies sollte im Normalfall schon direkt während des Refactorings passieren.

Einfache funktionale Tests

Es gibt viele Varianten, funktionale Tests für Webseiten zu erstellen, die unterschiedliche Anforderungen an Erstellbarkeit durch bestimmte Personengruppen, Wartbarkeit und langfristige Stabilität erfüllen. Unserer Erfahrung nach ist für diesen Anwendungsfall eine Kombination aus PHPUnit und Mink optimal. Denn diese funktionalen Tests müssen nicht langfristig stabil sein, sondern sollen die Funktionalität nur während des Refactorings sicherstellen. Danach können sie im Normalfall direkt wieder gelöscht oder durch sinnvollere Tests ersetzt werden. Die Anforderungen an stabile und wartbare funktionale Tests sind andere und erfordern häufig zumindest ein Refactoring des Frontend-Codes.

Ein funktionaler Test für einen erfolgreichen Log-in kann aussehen wie in Listing 1. Die verwendete Vaterklasse FeatureTest leitet wiederum von PHPUnit_Framework_TestCase ab – diese Tests integrieren sich damit in eventuell bereits existierende Tests mit PHP­Unit. Die Basisklasse initialisiert das Browsertestframework Mink und stellt einige einfache Helfermethoden zur Verfügung. Der komplette Code kann in unserer Beispielapplikation gefunden werden [3].

Listing 1

class LoginTest extends FeatureTest { use FeatureTest\UserHelper; public function testLogin() { $user = $this->createUser('kore', 'password', 'kore@example.com', 'Kore Nordmann'); $page = $this->visit('/'); $page->find('css', '#username')->setValue('kore'); $page->find('css', '#password')->setValue('password'); $page->find('css', '#submit')->press(); $page = $this->session->getPage(); $this->assertNotNull( $welcomeBox = $page->find('css', '.welcome'), 'Login failed' ); $this->assertContains("Hello Kore", $welcomeBox->getText()); } }

Der in Listing 1 gezeigte Code ist relativ einfach verständlich, und das API zur Browsersteuerung von Mink erlaubt noch viele weitere Interaktionen mit Webseiten. Die Verwendung von CSS-Selektoren ist meistens der sinnvollste Weg, um bei existierenden Webseiten nahezu beliebige Elemente zu adressieren. Hier werden darüber erst zwei Formularfelder mit den Daten eines zuvor angelegten Benutzers gefüllt und anschließend das Formular durch Bestätigung des entsprechenden Buttons abgeschickt. Danach überprüfen wir, ob auf der Folgeseite ein Willkommenstext zu finden ist – andernfalls ist scheinbar ein Fehler aufgetreten.

Diese sehr schwache Fehlerbehandlung zeigt ein übliches Problem von funktionalen Tests: Das Fehlschlagen eines einzelnen Tests verrät in den seltensten Fällen die U...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang