0251 / 590 837 15
info@a-coding-project.de

PHPUnit – PHP Code besser testen!

Bei komplexen Projekten kann schnell etwas schief gehen, was auf den ersten Blick nicht auffällt. Das könnten zum Beispiel Fehler in Berechnungen sein, die man auf Grund der Menge von Daten nicht von Hand nachrechnen kann. Um so etwas zu vermeiden gibt es das Unit-Testing. Für fast alle Programmiersprachen existieren bereits eigene Tools. In PHP macht dies PHPUnit.

Wie funktioniert Unit-Testing?

Im Endeffekt stellt man in PHP ein paar ungewöhnliche Situationen nach und vergleicht das Ergebnis mit einem, was man von Hand ausgerechnet hat. Um dies in PHP richtig nutzen zu können, sollte man auf Objektorientierung setzen oder zumindest den Code auf viele Funktionen aufgesplittet haben. Denn es werden immer nur einzelne Fukntionen und nicht das große Ganze getestet. Es wäre theoretisch auch möglich z.B. über file_get_contents Anfragen an weitere Seiten zu machen und so auch den HTML-Code zu vergleichen.

Nach dem man in seinem Quellcode Änderungen vorgenommen hat, kann mal alle Tests mit PHPUnit auf einmal testen und sieht, welche Tests nun fehlgeschlagen sind. Am sinnvollsten kann man diese Tests vor dem Schreiben einer Funktion schon definieren. So deckt man alle Fälle ab, die die Funktion regeln soll, und kann die Tests von fehlgeschlagen zu erfolgreich ändern.

Natürlich kann es vorkommen, dass man nicht alle möglichen Situationen erkannt hat. Sollte man eine solche aber feststellen und den Fehler beheben, so kann man sofort einen neuen Unit-Test anlegen, damit dieser tückische Fehler später nicht versehentlich wieder herein kommt.

Was genau macht PHPUnit?

Theoretisch könnte man sich eine solche Testsituation auch selbst zusammenbauen. PHPUnit ist aber eine Art Framework, die extra für das Unit-Testing ausgelegt wurde. Es bietet auf Dauer gesehen viel mehr Möglichkeiten, als ein kurz zusammengeschriebenes Script.

Installation

Man kann PHPUnit über PEAR oder PHAR bekommen und automatisch installieren. Wer XAMPP nutzt, kann In der Eingabeaufforderung (cmd) in den php-Ordner des XAMPP-Ordners wechseln (bei mir waren Admin-Rechte erforderlich). Das geht über den Befehl „cd pfad„.

Dort kann man als erstes PEAR auf die aktuelle Version bringen. Das geht so:

pear channel-update, pear.php.net

Und danach noch einmal

„pear upgrade pear“.

Nun können wir PHPUnit selbst installieren. Dafür müssen wir Channels zum PEAR hinzufügen. Das sind quasi Quellen, woher die Daten geladen werden können:

pear channel-discover pear.phpunit.de
pear channel-discover components.ez.no
pear channel-discover pear.symfony-project.com

Die Installation startet dann so:

pear install –force –alldeps phpunit/PHPUnit

Den ersten Test aufsetzen

Ab geht’s in den Code! Ihr könnt euch über Github einfach mal das ZIP-File herunterladen, darin sind auch erste Beispieltests enthalten. Diese stecken im Ordner Tests/_files. Da gibt es eine BankAccount.php, die eine eine Beispielklasse enthält. Aber werft doch mal einen Blick in die BankAccountTest.test.php:

<?php
/**
 * PHPUnit
 *
 * Copyright (c) 2002-2013, Sebastian Bergmann <sebastian@phpunit.de>.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *
 *   * Neither the name of Sebastian Bergmann nor the names of his
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * @package    PHPUnit
 * @author     Sebastian Bergmann <sebastian@phpunit.de>
 * @copyright  2002-2013 Sebastian Bergmann <sebastian@phpunit.de>
 * @license    http://www.opensource.org/licenses/BSD-3-Clause  The BSD 3-Clause License
 * @link       http://www.phpunit.de/
 * @since      File available since Release 2.3.0
 */

require_once 'PHPUnit/Framework/TestCase.php';
require_once 'BankAccount.php';

/**
 * Tests for the BankAccount class.
 *
 * @package    PHPUnit
 * @author     Sebastian Bergmann <sebastian@phpunit.de>
 * @copyright  2002-2013 Sebastian Bergmann <sebastian@phpunit.de>
 * @license    http://www.opensource.org/licenses/BSD-3-Clause  The BSD 3-Clause License
 * @link       http://www.phpunit.de/
 * @since      Class available since Release 2.3.0
 */
class BankAccountWithCustomExtensionTest extends PHPUnit_Framework_TestCase
{
    protected $ba;

    protected function setUp()
    {
        $this->ba = new BankAccount;
    }

    /**
     * @covers BankAccount::getBalance
     * @group balanceIsInitiallyZero
     * @group specification
     */
    public function testBalanceIsInitiallyZero()
    {
        $this->assertEquals(0, $this->ba->getBalance());
    }

    /**
     * @covers BankAccount::withdrawMoney
     * @group balanceCannotBecomeNegative
     * @group specification
     */
    public function testBalanceCannotBecomeNegative()
    {
        try {
            $this->ba->withdrawMoney(1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::depositMoney
     * @group balanceCannotBecomeNegative
     * @group specification
     */
    public function testBalanceCannotBecomeNegative2()
    {
        try {
            $this->ba->depositMoney(-1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::getBalance
     * @covers BankAccount::depositMoney
     * @covers BankAccount::withdrawMoney
     * @group balanceCannotBecomeNegative
     */
    /*
    public function testDepositingAndWithdrawingMoneyWorks()
    {
        $this->assertEquals(0, $this->ba->getBalance());
        $this->ba->depositMoney(1);
        $this->assertEquals(1, $this->ba->getBalance());
        $this->ba->withdrawMoney(1);
        $this->assertEquals(0, $this->ba->getBalance());
    }
    */
}

Es wird hier die Klasse PHPUnit_Framework_TestCase erweitert. Dies muss jedes Mal gemacht werden, wenn man einen neuen Test erstellen möchten. Die Funktion setUp wird vor den Tests ausgeführt. Darin kann man die Tests vorbereiten und so zum Beispiel die Datenbankverbindung aufbauen. Die öffentlichen Funktionen sind die Tests selbst. Diese kann man nach belieben erweitern.

In der Funktion kann man dann den Test für erfolgreich oder gescheitert erklären. In der Funktion testBalanceIsInitiallyZero wird das mittels assertEquals gemacht. Sind beide Werte gleich (0 und $this->ba->getBalance()) ist der Test erfolgreich, andernfalls fehlgeschlagen.

In den anderen beiden Funktionen wird einfach mit $this->fail() der Test als fehlgeschlagen markiert. Wird in einer Funktion gar nichts gemacht, ist der Test automatisch erfolgreich. Neben fail und assertEquals gibt es auch noch einige weitere Funktionen.

Test ausführen

Den Test ausführen kann man wieder über CMD. Dabei muss man einfach „phpunit “ gefolgt vom Pfad der Test-Datei eingeben. Bei mir sieht das so aus:

Wenn man das $this->fail() mal an den Anfang einer Funktion stellt, sieht man direkt, welcher Test fehlgeschlagen ist:

Deine Erfahrung mit Unit-Testing?

Wenn man von Anfang an seine Projekte mit Unit-Testing entwickelt kann man sich einige Fehler und Sucherei danach ersparen. Ich persönlich habe Unit-Testing bisher nur damals in meiner Abschlussarbeit im .net-Framework (NUnit) genutzt. Aber da ich PHPUnit in Kürze verwenden werde, habe ich es schon einmal getest.

Wie sind denn deine Erfahrung. Hast du schon einmal Unit-Testing genutzt, oder willst du es mal ausprobieren?