Testen von Datei-Uploads
Heute haben wir das Glück mal wieder einen Gastbetrag zu hören. Manuel Pichler von Qafoo ist so nett und erzählt uns etwas über das Testen von Datei Uploads. Also Bühne frei für Manuel. (PS: Wer den Artikel gerne auf Englisch lesen will, der findet ihn hier)
Eine Frage die mir in regelmäßigen Abständen immer wieder gestellt wird ist die, wie man mit PHP den Upload einer Datei sinnvoll testen kann. Deshalb möchte ich mit in diesem Post einmal genauer mit genau diesem Thema beschäftigen und ein bei PHP-Entwicklern leider recht unbekanntes Testing-Framework vorstellen.
Beginnen wir also mit dem Testing-Framework das wir alle kennen und in unserer täglichen Arbeit nutzen, `PHPUnit`__. Mit diesem Framework habe wir die Möglichkeit nahezu jeden Teil unserer Software zu testen. Dies gilt auch für Datei-Uploads, solange die zu testende Anwendung Uploads ausschließlich über die $_FILES-Variable umsetzt. In diesem Fall kann einfach in der „setUp()„ des Tests ein entsprechend präpariertes $_FILES-Array erzeugt werden, auf das die
zu testende Software dann zugreifen kann. :
// ... protected function setUp() { parent::setUp(); $_FILES = array( 'file_valid' => array( 'name' => 'foo.txt', 'tmp_name' => '/tmp/php42up23', 'type' => 'text/plain', 'size' => 42, 'error' => 0 ) ); } // ...
Doch glücklicherweise ist es in den meisten Fällen nicht ganz so einfach, da die zu testende Software auf die wesentlich sichereren PHP-Funktionen „is_uploaded_file()„ und „move_uploaded_file()„ zurückgreifen. In diesem Fall nützt aber das Manipulieren des $_FILES-Arrays nichts, denn beide Funktionen operieren auf einer anderen Ebene und nicht auf den im Userland modifizierten Daten. ::
class UploadExample { protected $dest; public function __construct($dest) { $this->dest = rtrim($dest, '/') . '/'; } public function handle($name) { if (is_uploaded_file($_FILES[$name]['tmp_name'])) { move_uploaded_file( $_FILES[$name]['tmp_name'], $this->dest . $_FILES[$name]['name'] ); } } }
Natürlich können wir diesen Teil der Anwendung nun mit einem schwergewichtigern Framework, wie zum Beispiel Selenium, testen. Was allerdings einige Reihe von Nachteilen mit sich bringt, den um einen solchen Test auszuführen benötigt man eine Vielzahl weiterer Komponenten, wie dem Webserver, dem Selenium-Server und sonstige Infrastruktur um eine lauffähige Version der Software bereistellen zu
können. All dies erhöht natürlich den erforderlichen Aufwand und die Laufzeit für die Durchführung eines Tests. Gleichzeitig birgt dies auch immer die Gefahr, dass durch irgendwelche Seiteneffekte False-Positives auftreten.
Eine Alternative ist das `PHP`__ Test Framework bzw. kurz PHPT, das zum Testen von PHP selbst, aber auch in vielen `PEAR`__- und `PECL`__-Projekten verwendet wird. PHPT kann als ein leichtgewichtiges Test-Framework angesehen werden, das durch eine einfache Syntax und ein sehr plakatives Beschreibungsformat leicht zu erlernen ist. Ein vollständige Dokumentation der PHPT-Syntax ist auf
der `PHP Quality Assurance`__ Webseite zu finden. Wir beschränken uns in diesem Artikel nur auf die benötigten Elemente. Ein ganz einfache PHPT-Test besteht dabei aus den folgenden drei Elementen:
# Einer Beschreibung des zu testenden Verhaltens: :: --TEST-- Example test case # Dem eigentlichen Produktivcode: :: --FILE-- <?php var_dump(strpos('Manuel Pichler', 'P')); var_dump(strpos('Manuel Pichler', 'Z')); # Und dem erwarteten Ergebniss: :: --EXPECT-- int(7) bool(false)
Aber das aller Beste ist, dass fast jeder PHPT bereits installiert hat, ohne es zu wissen, denn jede PEAR-Installation bringt bereits einen PHPT-Test-Runner mit, der über das folgende Kommando aufgerufen werden kann: ::
~ $ pear run-tests example-test-case.phpt Running 1 tests PASS Example test case[example-test-case.phpt] TOTAL TIME: 00:00 1 PASSED TESTS 0 SKIPPED TESTS
Nach dieser kurzen Einführung in PHPT wollen wir uns aber wieder unserem eigentlichen Problem zuwenden, dem Testen eines Datei-Uploads. Und genau hier liegt auch die große Stärke von PHPT verglichen mit anderen Testing-Frameworks. Mit PHPT-Tests kann nahezu jeder Zustand simuliert werden in dem einen Anwendung laufen könnten. Es ist möglich alle Einstellungen in der „php.ini„ geändert werden, einzelne Funktionen deaktiviert oder auch eine open_basedir Restriktion für einen Test gesetzt werden. Weiterhin ist es möglich mit PHPT alle
Eingabedaten zu manipulieren, die von Außen an den PHP-Prozess übergeben werden, was auch die diversen Superglobals wie $_FILES, $_POST, $_SERVER, $_ENV umfasst. Allerdings findet diese Änderung der Daten bereits auf einer früheren Ebene statt, so dass auch interne Funktionen wie „is_uploaded_file()„ und „move_uploaded_file()„ mit entsprechenden Testdaten ausgeführt werden können.
Um einen Datei-Upload sinnvoll testen zu können fehlt jetzt nur noch ein Baustein nämlich, wie simuliere man einen Upload mit PHPT? Hierfür benötigen wir das „–POST_RAW–„ Element, in dem wie es der Name schon sagt eine rohe HTTP-Post-Nachricht spezifiziert werden kann. Der einfachste Weg um an passende Testdaten zu kommen ist wohl der, mittels `Wireshark`__ den Upload einer Datei einfach aufzuzeichnen und den entsprechenden Teil einfach zu kopieren. Das folgende Listing zeigt einen solchen Mitschnitt: ::
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfywL8UCjFtqUBTQn ------WebKitFormBoundaryfywL8UCjFtqUBTQn Content-Disposition: form-data; name="file"; filename="example.txt" Content-Type: text/plain Qafoo provides quality assurance support and consulting ------WebKitFormBoundaryfywL8UCjFtqUBTQn Content-Disposition: form-data; name="submit" Upload ------WebKitFormBoundaryfywL8UCjFtqUBTQn--
Damit haben wir jetzt alle Informationen zusammen, um den Upload einer Datei mit PHPT zu testen können. Beginnen wir also mit der eigentlichen Test Beschreibung: ::
--TEST-- Example test emulating a file upload
Anschließend fügen wir den Wireshark Mitschnitt mit dem Datei-Upload in das
``--POST_RAW--`` Element der PHPT-Datei ein: :: --POST_RAW-- Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfywL8UCjFtqUBTQn ------WebKitFormBoundaryfywL8UCjFtqUBTQn Content-Disposition: form-data; name="file"; filename="example.txt" Content-Type: text/plain Qafoo provides quality assurance support and consulting ------WebKitFormBoundaryfywL8UCjFtqUBTQn Content-Disposition: form-data; name="submit" Upload ------WebKitFormBoundaryfywL8UCjFtqUBTQn--
Jetzt fehlt nur noch ein wenig Glue-Code der die Upload-Komponente ausführt und die eigentlichen Test-Assertions: ::
--FILE-- <?php require __DIR__ . '/UploadExample.php'; $upload = new UploadExample('/tmp'); $upload->handle('file'); var_dump(file_exists('/tmp/example.rst')); ?> --EXPECT-- bool(true)
Und fertig ist ein PHPT-Test, mit dem der Upload von Dateien getestet werden kann. Natürlich verifizieren wir das jetzt noch einmal durch Aufruf des „run-tests„ Kommandos: ::
~ $ pear run-tests upload-example.phpt Running 1 tests PASS Example test emulating a file upload[upload-example.phpt] TOTAL TIME: 00:00 1 PASSED TESTS 0 SKIPPED TESTS
Der Test läuft, wir haben unsere Aufgabe erfolgreich abgeschlossen.
Der eine oder andere Leser wird sich jetzt wahrscheinlich die Frage stellen, wie er denn nun die PHPT-Tests in seine bisherige Test-Infrastruktur am besten integrieren kann. Auch hierfür gibt es eine recht einfache Lösung. Ein doch eher recht unbekanntes Feature von `PHPUnit`__ ist die von Haus aus eingebaute Unterstützung für PHPT-Tests. Man muss sich also keine Gedanken über die Integration machen, sondern nur von den entsprechenden Test-Klassen des PHPUnit-Frameworks erben. Bei diesen Klassen Handelt es sich:
– PHPUnit_Extensions_PhptTestCase
– PHPUnit_Extensions_PhptTestSuite
Mit der „PHPUnit_Extensions_PhptTestCase„ Klasse kann eine einzelne PHPT-Datei referenziert werden, die dann von PHPUnit ausgeführt wird. Zu beachten ist hierbei aber, dass der Pfad zur PHPT-Datei absolut angegeben wird: ::
<?php require_once 'PHPUnit/Extensions/PhptTestCase.php'; class UploadExampleTest extends PHPUnit_Extensions_PhptTestCase { public function __construct() { parent::__construct(__DIR__ . '/upload-example.phpt'); } }
Dagegen kann man bei der „PHPUnit_Extensions_PhptTestSuite„ Klasse einfach ein Verzeichnis angeben und PHPUnit lädt dann automatisch alle „*.phpt„ Dateien in diesem Verzeichnis: ::
<?php require_once 'PHPUnit/Extensions/PhptTestSuite.php'; class UploadExampleTestSuite extends PHPUnit_Extensions_PhptTestSuite { public function __construct() { parent::__construct(__DIR__); } }
Mit diesen beiden mächtigen Werkzeugen an der Hand sollte es jetzt dem geneigten Leser möglich sein beliebig komplexe Upload-Szenarien mittels PHPT und PHPUnit testen zu können.
Im Anhang zu diesem Artikel finden Sie auch ein Beispiel für das Testen eines Datei-Uploads, der innerhalb eines Zend-Framework Controllers abgehandelt wird. Der große Vorteil des PHPT-Tests besteht darin, dass ein solcher Test durchgeführt werden kann, ohne das hierfür der umständliche Weg über einen Webserver und das HTTP-Protokoll gegangen werden muss.
Beispiele:
==========
Weitere Beispiele findet ihr unter:
https://github.com/Qafoo/blog-examples/tree/master/testing_file_uploads
Quellen:
==========
Geiler Artikel!
Endlich mal wieder ein technischer Artikel bei dem man wirklich etwas neues hinzulernen kann. Weiter so!
http://i.imgur.com/1qFOE.png -> mysql_connect(‚localhost‘, ‚phphatesme‘, ‚mysql_password‘) or die(„Bei der Verbindung zur Datenbank ist ein Fehler aufgetreten.“);
oder so ähnlich 😉
Sehr interessanter Artikel.
Vor allem der Hinweis, dass man PHPT in PHPUnit integrieren kann, nimmt einem die Abneigung ein weiteres Framework einzuführen.
http://qafoo.com/blog/013_testing_file_uploads_with_php.html