27. Mai 2011

FLOW3 und PHP Testing Tricks

Welcher Test ist der richtige?

In der Entwicklung mit FLOW3 spielen Tests eine große Rolle. Das Tests dabei wartbar und aussagekräftig sind, erfordert einiges an Erfahrung beim Test-Driven-Development. Wichtig ist zwischen den verschiedenen Arten von Tests zu unterscheiden und für den richtigen Einsatzzweck anzuwenden:

Unit-Tests sind ein wichtiger Bestandteil zur Steigerung der Qualität und für eine größere Sicherheit bei zukünftigen Änderungen. Dabei wird jeweils nur eine Code-Einheit (z.B. eine Klasse) ohne größere Abhängigkeiten getestet. Ein fehlgeschlagener Unit-Test kann die Frage nach dem genauen Ort eines Problems oder einer inkompatiblen Änderung beantworten. Auch sind Unit-Tests die eigentliche Grundlage für Test-Driven-Development mit einem Test-First-Ansatz (also zuerst einen Test schreiben der fehlschlägt, dann die Implementierung).

Aber: selbst wenn alle Unit-Tests laufen, heißt das noch nicht, dass das Gesamtsystem zuverlässig läuft. Ausserdem sind Unit-Tests aus meiner Erfahrung bei der ersten Implementierung von ganz neuen Features (z.B. ein neues Persistenz-Backend wie das CouchDB-Package) unzureichend und störend, da die Richtung der Entwicklung oftmals noch nicht klar ist und über viele Schichten des Systems hinweg gearbeitet wird.

Functional-Tests in FLOW3 sind ein noch recht neues Feature um das System mit (fast) allen Abhängigkeiten testen zu können. Dabei steht das komplette FLOW3 mit Dependency Injection, AOP etc. in einem speziellen Kontext Testing zur Verfügung. So kann z.B. einfach der Versand von E-Mails oder die Datenbankverbindung für Tests umkonfiguriert werden.

Gerade für das Implementieren neuer Features können am Anfang gut Functional-Tests für eine automatische Überprüfung der Ziele benutzt werden. Wer im Browser in einer Webanwendung immer dieselben Schritte macht bis der Code das richtige tut, sollte vielleicht überlegen, ob Functional-Tests nicht Zeit einsparen könnten.

Der wichtigste Punkt für Functional-Tests ist für mich aber die Sicherheit, dass ein Feature im kompletten System mit allen Packages und Abhängigkeiten funktioniert. Generell sollte für jedes Szenario einer User-Story, also jedes zentrale Feature einer Applikation ein Functional-Test hinterlegt werden. Dabei ist es nicht wichtig jeden Ausführungspfad zu testen, das können auch Unit-Tests erledigen.

Functional-Test Tricks

1. Partielles Mocken von Objekten

Dabei werden Abhängigkeiten eines Objekts zum Teil durch ein Mock ersetzt. Nützlich für schwierig zu konfigurierende und zu testende Abhängigkeiten, z.B. Mailversand, externe Webservices oder Simulation eines Requests (die Helper-Methode sendWebRequest der FunctionalTestCase-Klasse nutzt selber diesen Trick). Richtig angewendet kann man damit zwar viele Schichten des Systems testen, aber bestimmte Abhängkeiten ausschließen.

2. Fixture Factories für Testdaten

Natürlich brauchen Functional-Tests irgendwann auch ein fertig instanziiertes Model als Grundlage. Recht lästig wird es, wenn jedesmal bestimmte Eigenschaften gesetzt werden müssen und dieses dann in verschiedenen Tests wiederholt wird. Auch Tests sollten nicht zu viel Redundanz aufweisen. Änderungen am Model und Änderungen in der Validierung verursachen dann viel Arbeit.

Model. Bauen. Redundanz? Da gibt es doch dieses Factory-Pattern? Genau. Ein wunderbarer Anwendungsfall. Mit ein bisschen Flexibilität gepaart, können durch eine Fixture-Factory viele Zeilen Testcode vereinfacht werden.

Nehmen wir z.B. ein Customer-Model für Kundendaten. Eine solche Factory könnte eine Methode buildValidCustomer bereitstellen, die ein Objekt baut und gleich mit den richtigen Beispiel-Eigenschaften versieht. Wenn dann noch Eigenschaften in einem Array zum Überschreiben übergeben werden können, sind auch Tests mit Abhängigkeiten auf bestimmte Werte in Eigenschaften gut lesbar und unterschiedliche Varianten eines Objektes möglich.

Mit einer zusätzlichen Method createValidCustomer könnte man dann auch das Repository im Functional-Test sparen und das Objekt direkt zum Repository hinzufügen.

  1. class Customers {
  2.  
  3. protected $validCustomerProperties = array(
  4. 'customerNumber' => '123456',
  5. 'emailAddress' => 'john.doe@example.com',
  6. 'firstname' => 'John',
  7. 'lastname' => 'Doe'
  8. );
  9.  
  10. public function buildValidCustomer(array $overrideProperties) {
  11. $properties = array_merge($this->validCustomerProperties, $overrideProperties);
  12. $customer = new \F3\MyPackage\Domain\Model\Customer();
  13. foreach ($properties as $propertyName => $propertyValue) {
  14. if (\F3\FLOW3\Reflection\ObjectAccess::isPropertySettable($customer, $propertyName)) {
  15. \F3\FLOW3\Reflection\ObjectAccess::setProperty($customer, $propertyName, $propertyValue);
  16. }
  17. }
  18. return $customer;
  19. }
  20.  
  21. }
  1. class CustomersTest extends \F3\FLOW3\Tests\FunctionalTestCase {
  2.  
  3. static protected $testablePersistenceEnabled = TRUE;
  4.  
  5. /**
  6.   * @var \F3\MyPackage\Tests\Function\Fixtures\Domain\Model\Customers
  7.   */
  8. protected $customers;
  9.  
  10. public function setUp() {
  11. parent::setUp();
  12. $this->customers = $this->objectManager->get('F3\MyPackage\Tests\Function\Fixtures\Domain\Model\Customers');
  13. }
  14.  
  15. /**
  16.   * @test
  17.   * @expectedException \F3\FLOW3\Persistence\Generic\Exception\ObjectValidationFailedException
  18.   */
  19. public function customerEmailAddressHasToBeUnique() {
  20. $customer1 = $this->customers->buildValidCustomer(array('emailAddress' => 'foo@bar.com'));
  21. $customer2 = $this->customers->buildValidCustomer(array('emailAddress' => 'foo@bar.com'));
  22. $this->customerRepository->add($customer1);
  23. $this->customerRepository->add($customer2);
  24. $this->persistenceManager->persistAll();
  25. }
  26.  
  27. }

Was uns direkt zum nächsten "Trick" bringt: dem effektiven Testen von Exceptions in Tests.

4. Test von Exceptions

PhpUnit bringt eine einfache Möglichkeit mit sich Exceptions zu erwarten. Ein mit @expectedException (wie im Beispiel oben) annotierter Test stellt sicher, dass eine Exception mit dem angegebenen Typ geworfen wurde.

Was aber, wenn die Eigenschaften einer Exception relevant sind? Nehmen wir z.B. eine Exception, die bei Validierungen geworfen wird, und die ein Validierungsergebnis (F3\FLOW3\Error\Result) beinhaltet. Wenn ein Test sicherstellen soll, dass die richtige Eigenschaft als Fehlerhaft markiert wurde, macht es Sinn, die Exception abzufangen, die Testerwartung durch ein Assert anzugeben und die Exception wiederum zu werfen, damit ein @expectedException für uns den Rest erledigt und den Test gut lesbar dokumentiert.

  1. class CustomersTest extends \F3\FLOW3\Tests\FunctionalTestCase {
  2.  
  3. // ... Set up dependencies
  4.  
  5. /**
  6.   * @test
  7.   * @expectedException \F3\MyPackage\Exception\ValidationException
  8.   */
  9. public function createCustomerWithMissingEmailAddressThrowsValidationException() {
  10. try {
  11. $this->customerService->createCustomer($this->customers->buildValidCustomer('emailAddress' => ''));
  12. } catch (\F3\MyPackage\Exception\ValidationException $exception) {
  13. $this->assertTrue($exception->getResult()->forProperty('emailAddress')->hasErrors());
  14. throw $exception;
  15. }
  16. }
  17.  
  18. }

Testen testen testen ...

Es gibt noch viele weitere interessante Bereiche des Testings von Webanwendungen mit PHP und FLOW3. Dazu gehört sicherlich auch die richtige Anwendung von Mock-Objects und vor allem das Schreiben von aussagekräftigen und lesbaren Tests. Es bleibt also noch Platz für den einen oder anderen Folgeartikel...

Aus aktuellem Anlass: Wir suchen erfahrene PHP-Entwickler, die unser Team verstärken wollen und Lust auf spannende PHP-Projekte der nächsten Generation z.B. mit FLOW3 Entwicklung haben.

Trackback-Link
  •  
  • 3 Kommentar(e)
  •  
Gravatar: icke
icke
14. Mai 2012
Unbrauchbar

Sorry, aber der Post ist irgendwie völlig unbrauchbar. Das einzig sinnvolle ist Punkt 4.
Offene Fragen: Wie führt man die Tests eigentlich aus? Wie mockt man generell und speziell, z.B. Repositories? Wo ist Punkt 3? Das Package "Testing" wurde als 'deprecated' markiert, warum soll ich das nutzen?

PS: Die blöden Linien hier im Eingabefeld sind absolut störend, weil sie nicht mit der Zeilenhöhe übereinstimmen (Google Chrome)

Gravatar: Christopher
Christopher
20. Okt 2012
Unbrauchbar?

Punkt 3 ist anscheinend über Bord gegangen ;) Ansonsten geht es hier natürlich nicht um die Grundlagen von Unit Tests (Mocks werden bei PHPUnit erklärt), sondern um erweiterte Möglichkeiten im Einsatz mit Flow und vor allem Functional Tests. Wer schon mit Tests in Flow gearbeitet hat wird sicherlich das eine oder andere Nützliche finde.

Tests können über die mitgelieferten PHPUnit Konfigurationen ausgeführt werden:

cd Packages/Application/MyPackage
phpunit -c ../../../Build/Common/PhpUnit/UnitTests.xml Tests/Unit

Oder für Functional Tests:

cd Packages/Application/MyPackage
phpunit -c ../../../Build/Common/PhpUnit/FunctionalTests.xml Tests/Functional

Das Testing-Package wird übrigens schon längst nicht mehr benötigt, da die notwendigen Klassen in Flow enthalten sind.

P.S.: Die Zeilen im Hintegrund stören wirklich.

Gravatar: Patrick
Patrick
24. Mai 2012
Danke

Hi Christopher,
danke für den Beitrag. Ich habe das ganze in meinem Model so umgesetzt und ich denke es macht den Test-Code wesentlich übersichtlicher und wartbarer.

Einziger unterschied ist das ich die Factory Methoden static definiert habe.

VG
Patrick

Mein Kommentar

Benachrichtige mich, wenn jemand einen Kommentar zu dieser Nachricht schreibt.

Zurück