smart | Webentwicklung
Alles rund um HTML5, PHP, WordPress & Co.

PHPUnit: Dateisystem mocken mit vfsStream

3. September 2013
Stephan
PHPUnit: Dateisystem mithilfe von vfsStream mocken

Beim Unit-Testing möchte man gerne die zu testenden Units, wie z.B. einzelne Klassen isoliert vom Gesamtsystem und somit ohne äußere Einflüsse bzw. Abhängigkeiten testen. Dazu lassen sich Pseudo-Objekte, sogenannte Mocks, erstellen und verwenden.

Testet ihr z.B. eine Klasse A, die abhängig von Klasse B ist, so könnt ihr Klasse B mocken, damit Klasse A unabhängig von Klasse B getestet werden kann.

Das Mocken von Klassen bzw. konkreten Objekten unterstützt PHPUnit bereits „out of the box“. Schwieriger hingegen ist das Mocken des Dateisystems. Hierfür gibt es aber mit vfsStream eine einfache Lösung. Dabei handelt es sich um einen Stream-Wrapper, welcher Zugriff auf ein virtuelles Dateisystem ermöglicht und innerhalb von PHPUnit-Tests als Mock für das reale Dateisystem genutzt werden kann.

In diesem Artikel zeige ich euch an einem einfachen Beispiel, wie ihr vfsStream in euren PHPUnit-Tests einsetzen könnt.

Die zu testende Klasse

Als Beispiel dient eine Klasse, die zum Einlesen einer Datenbank-Konfigurationdatei dient. Der dazugehörige Code sieht wie folgt aus:

class DatabaseConfig
{
    protected $driver;
    protected $host;
    protected $port;
    protected $dbName;
    protected $user;
    protected $password;
    protected $path;

    public function __construct($filePath, $fileName)
    {
        $file = $filePath . DIRECTORY_SEPARATOR . $fileName;

        if(!file_exists($file))
        {
            throw new Exception('Database config file "' . $fileName . '" not found in "' . $filePath . '".');
        }

        $configData = parse_ini_file($file);

        $this->driver = $configData['driver'];
        $this->host = $configData['host'];
        $this->port = $configData['port'];
        $this->dbName = $configData['dbname'];
        $this->user = $configData['user'];
        $this->password = $configData['password'];
    }
}

Ich habe versucht die Klasse so einfach wie möglich zu halten. Deshalb besteht sie auch nur aus dem Konstruktor und einigen Eigenschaften.

Erstellen wir nun ein Objekt dieser Klasse, erwartet der Konstruktor den Pfad und den Namen der Konfigurationsdatei. Sollte die Datei nicht existieren, wird eine Exception geworfen. Andernfalls wird der Inhalt der Datei, bei der es sich um eine sogenannte Initialisierungsdatei handelt, eingelesen und die Daten den entsprechenden Eigenschaften zugewiesen.

Was wir nun möchten, ist die Erstellung von Unit-Tests für diese Klasse.

Tests mit realem Dateisystem

Als erstes wollen wir uns mal anschauen, wie die Tests ausschauen würden, wenn wir das Dateisystem nicht mocken.

class DatabaseConfigTest extends \PHPUnit_Framework_TestCase
{
    protected $filePath = 'd:\app\config';

    /**
     * @describe    '__construct'
     * @context     'when config file not exists'
     * @test
     */
    public function construct_shouldThrowExceptionWhenConfigFileNotExists()
    {
        $fileName = 'null.ini.php';
        
        if(file_exists($this->filePath . '\' . $fileName))
        {
            unlink($this->filePath . '\' . $fileName);
        }

        $this->setExpectedException(
            'Exception',
            'Database config file "null.ini.php" not found in "d:\app\config".'
        );

        $dbConfig = new DatabaseConfig(
            $this->filePath, 
            $fileName
        );
    }

    /**
     * @describe    '__construct'
     * @context     'when config file exists'
     * @test
     */
    public function construct_shouldAssignDriverToCorrespondingPropertyWhenConfigFileExists()
    {
        $fileName = 'db.ini.php';

        if(!file_exists($this->filePath))
        {
            mkdir($this->filePath);
        }

        if(!file_exists($this->filePath . '\' . $fileName))
        {
            file_put_contents($this->filePath . '\' . $fileName, '
                driver = "sqlite"
                host = "localhost"
                port = "3306"
                dbname = "fewobepaDB"
                user = "hans"
                password = "12345"
            ');
        }

        $dbConfig = new DatabaseConfig(
            $this->filePath, 
            $fileName
        );

        $this->assertAttributeSame(
            'sqlite',
            'driver',
            $dbConfig
        );

        if(file_exists($this->filePath . '\' . $fileName))
        {
            unlink($this->filePath . '\' . $fileName);
        }

        if(file_exists($this->filePath))
        {
            rmdir($this->filePath);
        }
    }
}

Vielleicht erkennt ihr schon das Problem beim Testen von Code, welcher mit dem Dateisystem agiert. Um z.B. zu testen, dass eine Exception geworfen wird, wenn die Datei nicht existiert, müssen wir sicherstellen, dass diese Datei auch wirklich nicht unter dem angebenen Dateipfad im Dateisstem existiert. Andererseits muss für den anderen Testfall gewährleistet sein, dass die Datei wirklich existiert.

Hierfür haben wir in beiden Testfällen zusätzlichen Code, der unsere Tests unnötig aufbläht. Der große Nachteil dabei ist aber auch, dass wenn z.B. der letzte Test fehlschlägt, das erstellte Verzeichnis und die erstellte Datei nicht wieder gelöscht werden.

Auch wenn sich die Klasse so testen lässt, ist das vom Code und von den Ahängigkeiten her nicht gut gelöst.

Tests mit vfsStream und virtuellem Dateisystem

Besser ist es, wenn wir nicht das reale Dateisystem nutzen, sondern für unsere Tests ein virtuelles Dateisystem einsetzen.

use org\bovigo\vfs\vfsStream,
    org\bovigo\vfs\vfsStreamWrapper;

class DatabaseConfigTest extends \PHPUnit_Framework_TestCase
{
    protected $filePath = 'app/config';

    protected function setUp()
    {
        vfsStream::setup('app');
        vfsStream::create(array(
            'config' => array(
                'db.ini.php' => '
                    driver = "sqlite"
                    host = "localhost"
                    port = "3306"
                    dbname = "fewobepaDB"
                    user = "hans"
                    password = "12345"
                '
            )
        ));
    }

    /**
     * @describe    '__construct'
     * @context     'when config file not exists'
     * @test
     */
    public function construct_shouldThrowExceptionWhenConfigFileNotExists()
    {
        $fileName = 'null.ini.php';

        $this->setExpectedException(
            'Exception',
            'Database config file "null.ini.php" not found in "vfs://app/config".'
        );

        $dbConfig = new DatabaseConfig(
            vfsStream::url($this->filePath), 
            $fileName
        );
    }

    /**
     * @describe    '__construct'
     * @context     'when config file exists'
     * @test
     */
    public function construct_shouldAssignDriverToCorrespondingPropertyWhenConfigFileExists()
    {
        $fileName = 'db.ini.php';

        $dbConfig = new DatabaseConfig(
            vfsStream::url($this->filePath), 
            $fileName
        );

        $this->assertAttributeSame(
            'sqlite',
            'driver',
            $dbConfig
        );
    }
}

Wie ihr erkennen könnt, sehen die Tests nun übersichtlicher aus und beschränken sich auf das Wesentliche. Der Unterschied ist, dass wir in der setUp-Methode ein virtuelles Dateisystem erstellen und nicht die Pfade des realen, sondern des virtuellen Dateisystems dem Konstruktor der DatabaseConfig-Klasse übergeben.

Weitere Artikel zum Thema

Im Folgenden habe ich euch noch ein paar Links zu Artikeln aufgelistet, die sich auch mit dem Mocken des Dateisystems auf Basis von vfsStream und PHPUnit beschäftigen:

Fazit

Mit vfsStream könnt ihr ganz einfach das reale Dateisystem mocken und so unnötige Abhängigkeiten beim Testen vermeiden.

Dabei könnt ihr zu Testzwecken „on the fly“ ohne viel Code ganze Verzeichnisstrukturen und Dateien mit dazugehörigem Inhalt erstellen. vfsStream bietet z.B. auch die Möglichkeit ein Abbild eines Verzeichnisses eures realen Dateisystems anzulegen.

Musstet ihr auch schon mal Code testen, der mit dem Dateisystem interagiert? Wie habt ihr das in euren Tests gelöst und was haltet ihr von vfsStream?

Kommentare  
2 Kommentare vorhanden
1 Yvonne schrieb am 5. September 2013 um 20:04 Uhr

Danke für den Code!
Sehr übersichtlich und nun leuchtet es echt ein.
Ich tuh mich aber auch immer schwer 😛

Danke und LG Yvonne

2 Stephan schrieb am 20. März 2015 um 08:47 Uhr

Meiner Vorrednerin kann ich leider nicht zustimmen. Ich bin noch nicht so richtig durchgestiegen. Kannst du mir Links nennen, wo ich mehr zum Thema finde?

0 Trackbacks/Pingbacks vorhanden
Du bist herzlich eingeladen auch ein Kommentar zu hinterlassen!
Kommentar schreiben

Vielen Dank für dein Kommentar!