Testgetriebene Entwicklung und Unit-Tests sind die neuesten Methoden, um sicherzustellen, dass Code trotz Änderungen und größeren Optimierungen weiterhin wie erwartet funktioniert. In diesem Artikel erfahren Sie, wie Sie Ihren PHP-Code auf Modul-, Datenbank- und Benutzeroberflächenebene (UI) einem Unit-Test unterziehen.
Es ist 3 Uhr morgens. Woher wissen wir, dass unser Code noch funktioniert?
Webanwendungen laufen rund um die Uhr, sodass mich die Frage, ob mein Programm noch läuft, nachts stören wird. Unit-Tests haben mir geholfen, so viel Vertrauen in meinen Code aufzubauen, dass ich ruhig schlafen kann.
Unit-Tests sind ein Framework zum Schreiben von Testfällen für Ihren Code und zum automatischen Ausführen dieser Tests. Testgetriebene Entwicklung ist ein Unit-Test-Ansatz, der auf der Idee basiert, dass Sie zunächst Tests schreiben und überprüfen sollten, ob diese Tests Fehler finden können, und erst dann mit dem Schreiben des Codes beginnen, der diese Tests bestehen muss. Wenn alle Tests bestanden sind, ist die von uns entwickelte Funktion abgeschlossen. Der Wert dieser Komponententests besteht darin, dass wir sie jederzeit ausführen können – vor dem Einchecken des Codes, nach einer größeren Änderung oder nach der Bereitstellung auf einem laufenden System.
PHP-Unit-Testing
Für PHP ist das Unit-Testing-Framework PHPUnit2. Dieses System kann als PEAR-Modul über die PEAR-Befehlszeile installiert werden: % pear install PHPUnit2.
Nach der Installation des Frameworks können Sie Komponententests schreiben, indem Sie Testklassen erstellen, die von PHPUnit2_Framework_TestCase abgeleitet sind.
Modul-Unit-Testing
Ich habe herausgefunden, dass der beste Ort, um mit Unit-Tests zu beginnen, das Geschäftslogikmodul der Anwendung ist. Ich verwende ein einfaches Beispiel: Dies ist eine Funktion, die zwei Zahlen summiert. Um mit dem Testen zu beginnen, schreiben wir zunächst den Testfall wie unten gezeigt.
Listing 1. TestAdd.php
<?phprequire_once 'Add.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAdd erweitert PHPUnit2_Framework_TestCase{ function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); } function test2() { $this->assertTrue( add( 1, 1 ) == 2 }}?>
Diese TestAdd-Klasse verfügt über zwei Methoden, die beide das Testpräfix verwenden. Jede Methode definiert einen Test, der so einfach wie Listing 1 oder sehr komplex sein kann. In diesem Fall behaupten wir einfach, dass 1 plus 2 im ersten Test gleich 3 und 1 plus 1 im zweiten Test gleich 2 ist.
Das PHPUnit2-System definiert die Methode „asserTrue()“, mit der getestet wird, ob der im Parameter enthaltene Bedingungswert wahr ist. Anschließend haben wir das Add.php-Modul geschrieben, das zunächst zu falschen Ergebnissen führte.
Listing 2. Add.php
<?phpfunction add( $a, $b ) { return 0 }?>
Beim Ausführen der Komponententests schlagen nun beide Tests fehl.
Listing 3. Testfehler
% phpunit TestAdd.phpPHPUnit 2.2.1 von Sebastian Bergmann.FFTime: 0.0031270980834961Es gab 2 Fehler:1) test1(TestAdd)2) test2(TestAdd)FAILURES!!!Tests ausgeführt: 2, Fehler: 2, Fehler: 0, Unvollständige Tests: 0.
Jetzt weiß ich, dass beide Tests gut funktionieren. Daher kann die Funktion add() so geändert werden, dass sie tatsächlich den eigentlichen Zweck erfüllt.
Beide Tests sind nun bestanden.
<?phpfunction add( $a, $b ) { return $a+$b; }?>
Listing 4. Test bestanden
% phpunit TestAdd.phpPHPUnit 2.2.1 von Sebastian Bergmann...Zeit: 0,0023679733276367OK (2 Tests)%
obwohl Dieses Beispiel einer testgetriebenen Entwicklung ist sehr einfach, aber wir können seine Ideen verstehen. Wir haben zuerst den Testfall erstellt und hatten genug Code, um den Test auszuführen, aber das Ergebnis war falsch. Anschließend überprüfen wir, ob der Test tatsächlich fehlschlägt, und implementieren dann den eigentlichen Code, damit der Test erfolgreich ist.
Ich stelle fest, dass ich beim Implementieren von Code so lange Code hinzufüge, bis ich einen vollständigen Test habe, der alle Codepfade abdeckt. Am Ende dieses Artikels finden Sie einige Ratschläge dazu, welche Tests Sie schreiben sollten und wie Sie sie schreiben.
Datenbanktests
Nach dem Modultest können Datenbankzugriffstests durchgeführt werden. Das Testen des Datenbankzugriffs wirft zwei interessante Probleme auf. Zunächst müssen wir die Datenbank vor jedem Test auf einen bekannten Punkt wiederherstellen. Zweitens müssen Sie sich darüber im Klaren sein, dass diese Wiederherstellung zu Schäden an der vorhandenen Datenbank führen kann. Daher müssen wir Tests mit einer Nicht-Produktionsdatenbank durchführen oder beim Schreiben von Testfällen darauf achten, den Inhalt der vorhandenen Datenbank nicht zu beeinträchtigen.
Das Testen von Datenbankeinheiten beginnt bei der Datenbank. Um dieses Problem zu veranschaulichen, müssen wir das folgende einfache Muster verwenden.
Listing 5. Schema.sql
DROP TABLE IF EXISTS Authors;CREATE TABLE Authors (ID MEDIUMINT NOT NULL AUTO_INCREMENT, Name TEXT NOT NULL, PRIMARY KEY (ID));
Listing 5 ist eine Autorentabelle und jeder Datensatz hat eine zugehörige ID.
Als nächstes können Sie Testfälle schreiben.
Listing 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAuthors erweitert PHPUnit2_Framework_TestCase{ function test_delete_all() { $this->assertTrue( Authors::delete_all() ); } function test_insert() { $this->assertTrue( Authors::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) } function test_insert_and_get() { $this->assertTrue( Authors ::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) ); $this->assertTrue( Authors::insert( 'Joe' ) ); ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == 2 );
Diese Reihe von Tests umfasst das Löschen von Autoren aus der Tabelle und das Einfügen von Autoren in die Tabelle Sowie Funktionen wie das Einfügen des Autors bei gleichzeitiger Überprüfung, ob der Autor existiert. Dies ist ein kumulativer Test, der meiner Meinung nach sehr nützlich ist, um Fehler zu finden. Indem Sie beobachten, welche Tests funktionieren und welche nicht, können Sie schnell herausfinden, was falsch läuft, und dann die Unterschiede besser verstehen.
Die Version des PHP-Datenbankzugriffscodes dblib.php, die ursprünglich den Fehler verursacht hat, wird unten angezeigt.
Listing 7. dblib.php
<?phprequire_once('DB.php'); class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; $db =& DB: :Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage() } return $db; { return false; } public static function insert( $name ) { return false; } public static function get_all() { return null }}?>
Das Ausführen von Unit-Tests für den Code in Listing 8 zeigt, dass alle drei Tests fehlschlagen:
Listing 8. dblib. php
% phpunit TestAuthors.phpPHPUnit 2.2.1 von Sebastian Bergmann.FFFTime: 0.007500171661377Es gab 3 Fehler:1) test_delete_all(TestAuthors)2) test_insert(TestAuthors)3) test_insert_and_get(TestAuthors)FAILURES!!!Tests run: 3, Failures: 3, Fehler: 0, Unvollständige Tests: 0, %
Jetzt können wir damit beginnen, den Code für den korrekten Zugriff auf die Datenbank hinzuzufügen – Methode für Methode – bis alle 3 Tests bestanden sind. Die endgültige Version des dblib.php-Codes wird unten angezeigt.
Listing 9. Vervollständigen Sie dblib.php
<?phprequire_once('DB.php');class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; $db =& DB ::Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage() } return $db; { $ db = Authors::get_db(); $sth = $db->prepare( 'DELETE FROM Authors' ); return true; Authors::get_db(); $sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' ); $db->execute( $sth, array( $name ) ); statische Funktion get_all() { $db = Authors::get_db(); $res = $db->query( "SELECT * FROM Authors" ); while( $res->fetchInto( $ row ) ) { $rows []= $row; } return $rows; }}?>
HTML-Test
Der nächste Schritt beim Testen der gesamten PHP-Anwendung besteht darin, die Front-End-Schnittstelle der Hypertext Markup Language (HTML) zu testen. Um diesen Test durchzuführen, benötigen wir eine Webseite wie die folgende.
Listing 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestPage erweitert PHPUnit2_Framework_TestCase{ function get_page( $url ) { $client = new HTTP_Client(); $client ->get( $url ); $resp = $client->currentResponse(); return $resp['body']; } function test_get() { $page = TestPage::get_page( 'http://localhost/unit /add.php' ); $this->assertTrue( strlen( $page ) > 0 ); $this->assertTrue( preg_match( '/<html>/', $page ) == 1 ); ) { $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' ); affirmTrue( preg_match( '/<html>/', $page ) == 1 ); preg_match( '/<span id="result">(.*?)</span>/', $page, $out ); $this->assertTrue( $out[1]=='30' ); }}?>
Dieser Test verwendet das von PEAR bereitgestellte HTTP-Client-Modul. Ich finde es etwas einfacher als die integrierte PHP Client URL Library (CURL), aber letztere kann auch verwendet werden.
Es gibt einen Test, der die zurückgegebene Seite prüft und feststellt, ob sie HTML enthält. Der zweite Test fordert die Summe von 10 und 20 an, indem der Wert in die angeforderte URL eingefügt wird, und überprüft dann das Ergebnis auf der zurückgegebenen Seite.
Der Code für diese Seite wird unten angezeigt.
Listing 11. TestPage.php
<html><body><form><input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> + <input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =<span id="result"><?php echo($_REQUEST ['a']+$_REQUEST['b']); ?></span><br/><input type="submit" value="Add" /></form></body></html >
Diese Seite ist ziemlich einfach. In den beiden Eingabefeldern werden die aktuellen, in der Anfrage bereitgestellten Werte angezeigt. Die Ergebnisspanne zeigt die Summe dieser beiden Werte. Das Markup markiert alle Unterschiede: Es ist für den Benutzer unsichtbar, aber für die Unit-Tests sichtbar. Unit-Tests benötigen also keine komplexe Logik, um diesen Wert zu finden. Stattdessen wird der Wert eines bestimmten Tags abgerufen. Auf diese Weise wird der Test bestanden, wenn sich die Schnittstelle ändert, solange die Spanne vorhanden ist.
Schreiben Sie wie zuvor zuerst den Testfall und erstellen Sie dann eine fehlerhafte Version der Seite. Wir testen auf Fehler und ändern dann den Inhalt der Seite, damit sie funktioniert. Die Ergebnisse sind wie folgt:
Listing 12. Test fehlgeschlagen und dann die Seite geändert
% phpunit TestPage.phpPHPUnit 2.2.1 von Sebastian Bergmann...Zeit: 0,25711488723755OK (2 Tests)%
Beide Tests können bestanden werden, was bedeutet, dass der Test Code Es funktioniert gut.
Wenn Sie Tests mit diesem Code ausführen, laufen alle Tests ohne Probleme, sodass wir sicher sein können, dass unser Code korrekt funktioniert.
Aber das Testen des HTML-Frontends hat einen Fehler: JavaScript. Der HTTP-Clientcode (Hypertext Transfer Protocol) ruft die Seite ab, führt jedoch kein JavaScript aus. Wenn wir also viel Code in JavaScript haben, müssen wir Unit-Tests auf Benutzeragentenebene erstellen. Der beste Weg, diese Funktionalität zu erreichen, ist meiner Meinung nach die Verwendung der in Microsoft® Internet Explorer® integrierten Automatisierungsebenenfunktionalität. Mit in PHP geschriebenen Microsoft Windows®-Skripten können Sie die Component Object Model (COM)-Schnittstelle verwenden, um Internet Explorer so zu steuern, dass er zwischen Seiten navigiert, und anschließend Document Object Model (DOM)-Methoden verwenden, um Seiten zu finden, nachdem bestimmte Benutzeraktionen ausgeführt wurden .
Dies ist die einzige mir bekannte Möglichkeit, Front-End-JavaScript-Code einem Unit-Test zu unterziehen. Ich gebe zu, dass es nicht einfach ist, sie zu schreiben und zu pflegen, und diese Tests können leicht fehlschlagen, wenn auch nur geringfügige Änderungen an der Seite vorgenommen werden.
Welche Tests man schreibt und wie man sie schreibt
. Beim Schreiben von Tests decke ich gerne die folgenden Szenarien ab:
Alle positiven Tests
Diese Reihe von Tests stellt sicher, dass alles wie erwartet funktioniert.
Bei allen negativen Tests
werden diese Tests einzeln verwendet, um sicherzustellen, dass jeder Fehler oder jede Anomalie getestet wird.
Positive Sequenztests
Diese Reihe von Tests stellt sicher, dass Aufrufe in der richtigen Reihenfolge wie erwartet funktionieren.
Negativsequenztests
Diese Reihe von Tests stellt sicher, dass Anrufe fehlschlagen, wenn sie nicht in der richtigen Reihenfolge erfolgen.
Belastungstests
Gegebenenfalls kann eine kleine Reihe von Tests durchgeführt werden, um festzustellen, ob die Leistung dieser Tests unseren Erwartungen entspricht. Beispielsweise sollten 2.000 Anrufe innerhalb von 2 Sekunden abgeschlossen werden.
Ressourcentests
Diese Tests stellen sicher, dass die Anwendungsprogrammierschnittstelle (API) Ressourcen korrekt zuweist und freigibt – beispielsweise durch mehrmaliges Aufrufen der dateibasierten API zum Öffnen, Schreiben und Schließen, um sicherzustellen, dass keine Dateien mehr geöffnet sind.
Rückruftests
Bei APIs mit Rückrufmethoden stellen diese Tests sicher, dass der Code normal ausgeführt wird, wenn keine Rückruffunktion definiert ist. Darüber hinaus können diese Tests auch sicherstellen, dass der Code weiterhin normal ausgeführt werden kann, wenn Rückruffunktionen definiert sind, diese Rückruffunktionen jedoch fehlerhaft funktionieren oder Ausnahmen generieren.
Hier sind einige Gedanken zum Unit-Testen. Ich habe ein paar Vorschläge zum Schreiben von Unit-Tests:
Verwenden Sie keine Zufallsdaten
.Obwohl die Generierung von Zufallsdaten in einer Schnittstelle eine gute Idee zu sein scheint, möchten wir dies vermeiden, da das Debuggen dieser Daten sehr schwierig werden kann. Wenn die Daten bei jedem Aufruf zufällig generiert werden, kann es vorkommen, dass bei einem Test ein Fehler auftritt, bei einem anderen jedoch nicht. Wenn Ihr Test Zufallsdaten erfordert, können Sie diese in einer Datei generieren und diese Datei bei jeder Ausführung verwenden. Mit diesem Ansatz erhalten wir einige „verrauschte“ Daten, können aber trotzdem Fehler debuggen.
Gruppentests
Wir können problemlos Tausende von Tests ansammeln, deren Ausführung mehrere Stunden dauert. Daran ist nichts auszusetzen, aber die Gruppierung dieser Tests ermöglicht es uns, schnell eine Reihe von Tests durchzuführen und nach größeren Bedenken zu suchen und dann die gesamte Reihe von Tests nachts durchzuführen.
Robuste APIs und robuste Tests schreiben
Es ist wichtig, APIs und Tests so zu schreiben, dass sie nicht so schnell kaputt gehen, wenn neue Funktionalität hinzugefügt oder bestehende Funktionalität geändert wird. Es gibt kein universelles Allheilmittel, aber als Faustregel gilt, dass Tests, die „oszillieren“ (manchmal scheitern, manchmal erfolgreich sein, immer wieder), schnell verworfen werden sollten.
Fazit
Unit-Tests sind für Ingenieure von großer Bedeutung. Sie bilden eine Grundlage für den agilen Entwicklungsprozess (bei dem der Codierung große Bedeutung beigemessen wird, da für die Dokumentation ein gewisser Nachweis erforderlich ist, dass der Code gemäß den Spezifikationen funktioniert). Unit-Tests liefern diesen Beweis. Der Prozess beginnt mit Unit-Tests, die die Funktionalität definieren, die der Code implementieren soll, derzeit aber nicht implementiert. Daher werden alle Tests zunächst fehlschlagen. Wenn der Code dann fast fertig ist, werden die Tests bestanden. Wenn alle Tests erfolgreich sind, ist der Code sehr vollständig.
Ich habe noch nie großen Code geschrieben oder einen großen oder komplexen Codeblock geändert, ohne Komponententests zu verwenden. Normalerweise schreibe ich Komponententests für vorhandenen Code, bevor ich ihn ändere, nur um sicherzustellen, dass ich weiß, was ich kaputt mache (oder nicht kaputt mache), wenn ich den Code ändere. Das gibt mir großes Vertrauen, dass der Code, den ich meinen Kunden zur Verfügung stelle, korrekt funktioniert – sogar um 3 Uhr morgens.