Erweiterte Basisklassen für das Testen bestimmter Aspekte im Eigenbau

PHPUnit Basisklassen für symfony2-Projekte
Kommentare

Mit PHPUnit hat PHP vor langer Zeit ein Testframework an die Hand bekommen, das Unittests handhabbar macht. symfony2, ein „Framework für Web Projekte“ [1], nutzt PHPUnit sowohl für Unittests als auch für komplexere funktionale Tests. Für diese bringt symfony2 eine eigene Basisklasse mit. Im Folgenden sollen erweiterte Basisklassen für das Testen bestimmter Aspekte vorgestellt werden.

Um die folgenden Codebeispiele nutzen zu können, wird PHPUnit (getestet wurde mit Version 3.6) und eine funktionierende symfony2-Applikation (hier war die Testversion 2.0.12) vorausgesetzt.

Unittests ohne besondere Anforderungen

Für „einfache“ Unittests genügt es meist, die PHPUnit_Framework_TestCase Klasse abzuleiten. Es empfiehlt sich trotzdem, eine eigene abstrakte Klasse zu definieren. Dies hat zwei Gründe:

  • Da sich die API von PHPUnit in Zukunft ändern kann, macht man sich unabhängiger und kann seine Wrapper-Klasse zum Mappen von altem Code auf eine neue PHPUnit-Version nutzen.
  • Methoden, die vielen oder allen Unittests zur Verfügung gestellt werden sollen, können ohne großen Aufwand implementiert werden.

Die Klasse für UnitTests ist zu Beginn minimalistisch:

namespace AcmeDemoBundleTests;

abstract class UnitTestCase extends PHPUnit_Framework_TestCase {

}

Eine Testklasse, die Unittests durchführt, extendet von nun an die UnitTestCase Klasse. Wie schon beschrieben, können Funktionen, die in vielen Unittests benötigt werden, hier bereitgestellt werden. Ein Anwendungsfall ist z.B. die Verwendung von Reflections zum Auslesen oder Setzen von privaten Attributen einer Klasse. Wir könnten hierfür die Testklasse wie folgt erweitern:

abstract class UnitTestCase extends PHPUnit_Framework_TestCase {

    protected function getAttribute($object, $name) {
        $reflectionProperty = new ReflectionProperty(get_class($object), $name);
        $reflectionProperty->setAccessible(true);
        return $reflectionProperty->getValue($object);
    }

    protected function setAttribute($object, $name, $value) {
        $reflectionProperty = new ReflectionProperty(get_class($object), $name);
        $reflectionProperty->setAccessible(true);
        $reflectionProperty->setValue($object, $value);
    }

}

Sollte es nun in einem Test nötig sein, auf ein nicht zugängliches Attribut zuzugreifen, kann dies mittels $this->getAttribute($testObject, ‚privateAttribute‘);
erfolgen. Das Setzen eines Attributes funktioniert ähnlich: $this->setAttribute($testObject, ‚privateAttribute‘, ’someValue‘);

Einbindung einer Datenbank

Eine Entity mittels Unittests zu testen ist ein erster und wichtiger Schritt. Da Entities aber zum großen Teil aus Annotations bestehen und sehr eng mit doctrine2 verknüpft sind, sollten sie immer auch mit einer echten Datenbank getestet werden. Das stellt sicher, dass Datentypen korrekt gewählt sowie notnull und unique constraints gesetzt wurden und Schlüsselbeziehungen stimmen. Auch Lifecycle-Events können so getestet werden.

Um solche Tests unabhängig zu machen, gibt es zwei Möglichkeiten:

  1. Löschen der evtl. vorhandenen Datenbank, diese neu erstellen und Fixtures laden.
  2. Sicherstellen, dass eine Datenbank mit aktuellem Schema vorhanden ist, Ausführen der Tests innerhalb einer Transaktion und vor Beendigung des Tests einen Rollback durchführen.

Aus Gründen der Einfachheit habe ich mich für die erste Variante entschieden. Um die Test zu beschleunigen, wird in der Testumgebung eine SQLite Datenbank verwendet. Dies kann in symfony2 mittels entsprechender Konfiguration in der app/config/config_test.yml erreicht werden.

Die von symfony2 bereitgestellte WebTestCase Klasse wird normalerweise zum Durchführen von Controllertests mittels einem simulierten Browser benutzt [2]. Sie kann aber, wie in diesem Fall, als Grundlage für andere Testarten genutzt werden. Das Initialisieren und Löschen der Datenbank wird mittels bekannter Konsolenbefehle (z.B. php app/console doctrine:schema:create) durchgeführt. Die finale EntityTestCase Klasse sieht wie folgt aus:

namespace AcmeDemoBundleTests;

use SymfonyBundleFrameworkBundleConsoleApplication;
use SymfonyComponentConsoleInputArrayInput;

abstract class EntityTestCase extends SymfonyBundleFrameworkBundleTestWebTestCase {

    protected static $application;
    protected $entityManager;

    public static function setUpBeforeClass() {
        static::$kernel = static::createKernel();
        static::$kernel->boot();

        static::$application = new Application(static::$kernel);
        static::$application->setAutoExit(false);

        static::createDatabase();
    }

    public static function tearDownAfterClass() {
        static::executeCommand("doctrine:schema:drop", array("--force" => true));
    }

    public function setUp() {
        static::$kernel = static::createKernel();
        static::$kernel->boot();

        $this->entityManager = static::$kernel->getContainer()->get('doctrine')->getEntityManager();
    }

    private static function createDatabase() {
        static::executeCommand("doctrine:schema:create");
        static::executeCommand("doctrine:fixtures:load");
    }

    private static function executeCommand($command, Array $options = array()) {
        $options["-e"] = "test";
        $options["-q"] = null;
        $options = array_merge($options, array('command' => $command));
        return static::$application->run(new ArrayInput($options));
    }

}

Hevorgehoben werden soll die Tatsache, dass die Datenbank innerhalb von setUpBeforeClass initialisiert wird, also nicht vor jedem Test. Der EntityManager wird aber vor jedem Test neu initialisiert, da ansonsten z.B. eine PDOException zu einem Fehler in allen weiteren Testfällen führt. Dadurch wird sichergestellt, dass vor jedem Testcase der Cache des EntityManager geleert wird.

Es kann nötig sein, die Datenbank vor jedem Testfall neu zu erstellen. Das kann zu Performance-Einbußen führen, weshalb ich nach Möglichkeit davon abrate.


Themen der nächsten Seiten

  • Funktionale Tests für Services
  • Den Browser simulieren
  • Testklassen für verschiedenste Anwendungsfälle
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -