1. Magazin
  2. /
  3. Programmierung
  4. /
  5. Unit Testing in Laravel 5.5 – Unit Tests mit PHPUnit

Unit Testing in Laravel 5.5 – Unit Tests mit PHPUnit

Unit Testing in Laravel 5.5

Unit Testing in Laravel 5.5 – Unit Tests mit PHPUnit

Unit Testing ist wichtig und das scheint Dir schon bewusst zu sein, da Du es bis zu diesem Artikel geschafft hast. Die Gründe Unit Tests in Softwareprojekte zu integrieren wurden schon oft erläutert und sind Dir vormutlich auch bereits bekannt. In diesem Artikel werde ich Dir zeigen, wie Du Unit Tests mit dem PHP Test-Framework PHPUnit 6.4 in dem PHP Framwork Laravel 5.5 integrierst. Du wirst in Deinem Laravel Projekt über Artsian Unit Tests anlegen und diese mit Test-Logik befüllen.

Danach zeige ich Dir noch, wie Du automatisch ansehliche Code Coverage Reports für Dein Laravel Projekt generieren kannst, und dass alles mit Hilfe von PHPUnit. So hast Du immer die Testabdeckung Deines PHP Codes im Auge. Zum Schluss ergänze ich noch einige Fehler, die auftreten können, wo deren Ursprung oft nicht leicht zu ermitteln ist.

Einführung in das Unit Testing in Laravel

Laravel liefert von Haus aus Unit Testing und Integrationstest (Feature Tests) Methoden aus. Das starke PHP Test Framework PHPUnit ist fest in Laravel integriert und wird in der aktuellen Laravel Version 5.5 mit der Version 6.4 ausgeliefert. PHPUnit 6.4 unterstützt PHP 7.0 und PHP 7.1.

PHPUnit wird über den PHP Abhängigkeitsmanager Composer ausgeliefert und liegt nach dem „install“ Befehl im Verzeichnis „vendor/bin/“ (phpunit.php).
Ebenfalls mit an Board ist eine von Laravel bereitgestellte PHPUnit Konfigurationsdatei „phpunit.xml“. Diese liegt im Basisverzeichnis Deines Projektes. Über die „phpunit.xml“ kannst Du „environment“ Variablen überschreiben, „Testsuites“ für unterschiedliche Test-Kategorien anlegen und vieles mehr. Hier wirst Du später auch den automatischen PHP Code Coverage Report konfigurieren.

Laravel stellt in der „phpunit.xml“ Konfiguration bereits Test-Suites für Unit Tests und Integrationstests (Feature Tests) bereit. In Deinem Laravel-Projekt findest Du im Verzeichnis „tests“ bereits die beiden Test-Suite Verzeichnisse „Unit“ und „Feature“ in denen Du später die jeweiligen Tests hinterlegen kannst.
Im „tests“ Verzeichnis sind neben den Test-Suites auch die zwei Klassen „TestCase.php“ und „CreatesApplication.php“ hinterlegt. Diese „booten“ für Dich während der Tests die Laravel-Anwendung. Näheres dazu aber in den folgenden Abschnitten.

Laravel Testumgebung

Wenn Du Deine Tests mit PHPUnit ausführst, läuft die Laravel-Anwendung durch die überschreibenden Environment Variablen der „phpunit.xml“ im „Test-Modus“. Innerhalb dieses „Test-Modus“ ändert Laravel automatisch den Cache Treiber in den Modus „array“. Der Treiber dient zur Cache und Session-Verwaltung. Den Treiber kannst Du auch selber anpassen. Du findest ihn im Verzeichnis „config“ unter dem Name „cache.php“. Der „array“ Modus sorgt dafür, dass der Cache und die Sessions nicht persistent gespeichert werden (d.h. in der Datenbank oder im Dateisystem).

Du kannst die Konfiguration nach Deinen Wünschen anpassen und um weitere Einstellungen erweitern. Wichtig ist nur, dass Du nach vorgenommenen Änderungen den Laravel Konfig-Cache leerst. Dies kannst Du mit Artisan über den Befehl „config:clear“ tun:

$ php artisan config:clear

Einen Unit Test in Laravel anlegen und Tests ausführen

Wie bei fast allem hilft Dir auch hier das Laravel Tool Artisan aus. Du kannst den „make“ Befehl mit dem Parameter „test“ ausführen und einen Namen für den Test angeben. Wie das in der Konsole aussieht siehst Du anhand des folgendne Beispiels:

$ php artisan make:test FooBarTest --unit

Über den Zusatz „–unit“ wird ein Unit Test angelegt, lässt man ihn weg erhält man einen Integrationstest / Feature Test. Der Test wurde im jeweiligen Ordner angelegt. In diesem Beispiel wurde der Test im Unit Test Ordner abgelegt.
Wenn Du den Test öffnest kannst Du mit Hilfe aller PHPUnit Funktionen Unit Tests erstellen um Deine Code Units zu testen. Hier siehst Du noch mal den angelegten Unit Test:

<?php

namespace TestsUnit;

use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;

class FooBarTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $this->assertTrue(true);
    }
}

Solltest Du Deine eigene „setUp“ Funktion zum vorbereiten Deiner Tests erstellen, dann musst Du daran denken, die Funktion „parent:setUp“ mit aufzurufen, damit die Tests funktionieren.
Um nun Deinen frisch angelegten Unit Tests auszuführen, musst Du PHPUnit ausführen. Dazu gibt es mehrere Wege. Im Falle, dass Du PHPUnit nicht installiert hast, kannst Du die von Laravel mitgelieferte PHPUnit Datei „vendor/bin/phpunit.php“ über den folgenden Befehl in Deinem Laravel-Projektverzeichnis nutzen:

$ php ./vendor/bin/phpunit

Die zweite Möglichkeit ist, dass Du Dir PHPUnit in der benötigten Version 6.4.3 installierst und PHPUnit dann einfach über den folgenden Befehl in Deinem Laravel-Projektverzeichnis ausführst:

$ phpunit

Danach solltest Du ein ähnliches Ergebnis wie folgendes erhalten:

$ phpunit
PHPUnit 6.4.3 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 348 ms, Memory: 20.00MB

OK (2 tests, 2 assertions)

Komplettes Beispiel für Unit Tests in Laravel

Um das Ganze für Dich noch ein Stück greifbarer zu gestalten, werde ich Dir anhand von „Produkten“ zeigen, wie Du Deine Unit Tests aufbauen kannst. Du wirst in diesem Abschnitt ein Produkt Model und eine zugehörige Migration anlegen und einen passenden Controller dafür genierieren. Im Controller wirst Du passente CRUD (Create, Read, Update, Delete) Operationen, sowie die „Kaufen“ Funktion integrieren, die die Anzahl des gekauften Produktes reduziert.

Diese Funktionen wirst Du im Anschluss mit Unit Tests testen. Du wirst außerdem die „phpunit.xml“ um Environment Konfigurationen erweitern, damit nicht die „produktive“ Datenbank, sondern eine einfache SQLite Datenbank zum Testen verwendet wird. Wichtig ist, dass Du Dir SQLite3 und die PHP Extension zu SQLite3 installierst, sollte das nooch nicht der Fall sein. Unter Linux kannst Du dies mit den folgenden drei Befehlen tun:

$ sudo apt-get install sqlite3

Hier musst Du Deine installierte PHP Version kennen:

$ sudo apt-get install php7.0-sqlite3

Zum Schluss musst Du noch kurz den Apache Server neuladen:

$ sudo service apache2 reload

Du startest nun mit den Einträgen in der „phpunit.xml“. Ergänze die nachfolgenden Einträge in den „php“-Knoten:

<env name=“DB_CONNECTION“ value=“sqlite“ />
<env name=“DB_DATABASE“ value=“:memory:“ />

Als nächstes generierst Du mit Hilfe von Artisan das Produkt Model und die zugehörige Datenbank Migrationsdatei. Der Konsolenbefehl dazu lautet wie folgt:

$ php artisan make:model Product -m

Durch den Parameter „-m“ wird die Migration angelegt. Das Produkt wird sehr einfach aufgebaut sein, um die Komplexität in diesem Artikel gering zu halten. Du ergänzt gleich in der Migration die Spalten: Titel (string), Preis (decimal) und Anzahl (integer). Die erstellte Migration findest Du im Verzeichnis „database/migrations/“ und sie sollte einen ähnlichen Dateinamen wie folgenden tragen „2017_10_31_111518_create_products_table.php“. Mit den Spalten sollte Deine Migrationsdatei wie folgt aussehen:

<?php

use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            $table->string('title');
            $table->decimal('price', 6, 2);
            $table->integer('stock');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('products');
    }
}

Nach dem Du die Migrationsdatei befüllt hast, solltest Du nun die „fillable“ Eigenschaften im Model hinterlegen. Dazu öffnest Du das Model „Product.php“ im Verzeichnis „app“ und fügst folgende Variable ein:

<?php

namespace App;

use IlluminateDatabaseEloquentModel;

class Product extends Model
{
    protected $fillable = ['title', 'price', 'stock'];
}

In den Unit Testings musst Du später die Produkte „mocken“ oder „faken“. Dafür bringt Laravel eine „Faker“ Library mit. Diese Library benötigt eine „Factory“ zu dem Model Produkt, damit sie die richtigen Inhalte anlegen kann. Eine Factory für das Produkt legst Du über folgen Befehl an:

$ php artisan make:factory ProductFactory --model=Product

Artisan hat nun für Dich die Factory „ProductFactory.php“ unter „database/factories“ erstellt. Diese öffnest Du nun und fügst folgende „return“ Anweisung hinzu:

<?php

use FakerGenerator as Faker;

/* @var IlluminateDatabaseEloquentFactory $factory */

$factory->define(AppProduct::class, function (Faker $faker) {
    return [
        'title' => $faker->text(20),
        'price' => $faker->randomFloat(2),
        'stock' => $faker->randomNumber()
    ];
});

Jetzt bist Du soweit, dass Du den Unit Test anlegen kannst. Wie im ersten Teil dieses Beitrages, hilft Dir nun wieder Artisan:

$ php artisan make:test ProductTest --unit

Im Unit Test „ProductTest.php“ fügst Du nun die beiden Namespaces „AppProduct“ und „IlluminateFoundationTestingDatabaseMigrations“ hinzu und ergänzt zu Beginn der Klasse „use DatabaseMigrations;“ um die Datenbank Migrationen durchzuführen. Dadurch kannst Du nun in der SQLite Datenbank die in Deinem Hauptspeicher läuft Datenbankoperationen durchführen und so tun, als würdest Du auf der produktiven Datenbank arbeiten.

Danach legst Du den Unit Test „test_product_can_be_created“ als Funktion an und „fakest“ mit der zuvor erstellten Factory ein Produkt, dass direkt im Anschluss per „create“ in der SQLite Datenbank gespeichert wird.

Zu guter Letzt prüfst Du per „assertDatabaseHas“, ob das erstellte Produkt auch in der Datenbank gespeichert wurde.
Deine Unit Testing Sammlung sollte nun wie folgt aussehen:

<?php

namespace TestsUnit;

use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
use AppProduct;
use IlluminateFoundationTestingDatabaseMigrations;

class ProductTest extends TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function test_product_can_be_created()
    {
        $product = factory(Product::class)->create();

        $this->assertDatabaseHas('products', [
            'id'    => 1,
            'title' => $product->title,
            'price' => $product->price,
            'stock' => $product->stock
        ]);
    }
}

Den Unit Test führt Du nun mit $ phpunit aus, wie ich es Dir zu Begin des Artikels gezeigt habe.
Sollten hier nun Fehler auftreten, wie bspw. „IlluminateDatabaseQueryException: could not find driver (SQL: PRAGMA writable_schema = 1;)“, dann hast Du vermutlich vergessen SQLite3 und/oder die PHP Extension zu SQLite3 zu installieren. Weiter oben findest Du drei Befehle zur installation unter Linux. Weitere vorkompilierte Binaries findest Du unter „SQLite – Download“.

Um nun noch den Fall zu testen, dass auch Produkte gekauft werden können, legst Du den passenden Produkt Controller über Artisan an:

$ php artisan make:controller ProductController

Zuerst fügst Du im im Controller wieder den Namespace „AppProduct“ hinzu und im Anschluss erstellst Du die Funktion „buy_product“ mit den Parametern „id“ und „quantity“:

<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;
use AppProduct;

class ProductController extends Controller
{
    public function buy_product($id, $quantity) {
        $product = Product::findOrFail($id);

        if($quantity <= 0) {
            return response()->json( [
                'error'     => true,
                'message'   => 'Es muss mindestens ein Produkt bestellt werden.'
            ], 403 );
        } else if ($quantity > $product->stock) {
            return response()->json( [
                'error'     => true,
                'message' => 'Es sind leider nicht genügend Produkte auf Lager um diese Bestellung durchzuführen.'
            ], 403 );
        }

        $product->stock -= $quantity;
        $product->save();

        return response()->json( [
            'error'     => false,
            'message' => 'Produkte erfolgreich gekauft.'
        ], 200 );
    }
}

Damit Du die Funktion „buy_product“ aufrufen kannst, musst Du die Route konfigurieren. Öffne dazu die Datei „web.php“ im Verzeichnis „routes“. Dort ergänzt Du folgende Route:

Route::get('/products/{id}/buy/{quantity}', 'ProductController@buy_product');

Integriert wird hier eine Route die mit „/products/“ beginnt, danach muss eine Produkt ID übergeben werden. Im Anschluss folgt die Route „/buy/“ und die Anzahl der Produkte die gekauft werden sollen. Wird diese Route über HTTP Get aufgerufen wird die Funktion „buy_product“ im ProductController ausgeführt und die in der URL übergebenen Parameter an die Funktion übergeben.

Jetzt kannst Du für die folgenden Scenarien Test Funktionen anlegen, um den kompletten Kaufprozess und alle Möglichen Code Pfade zu Prüfen:

  • Das Produkt kann gekauft werden und der Warenbestand verringert sich
  • Das Produkt in der URL existiert nicht
  • Bei einem leeren Warenbestand kann das Produkt nicht gekauft werden
  • Es wurde keine Anzahl in der URL übergeben, obwohl mindestens ein Produkt gekauft werden muss

Diese drei Scenarien könnten wie folgt getestet werden:

/**
 * @test
 * Das Produkt kann gekauft werden und der Warenbestand verringert sich
 */
public function test_product_can_be_bought()
{
    $product = factory(Product::class)->create(['stock' => 16]);

    $response = $this->get('/products/1/buy/15');
    $response->assertStatus(200);

    $result = Product::find(1);
    $this->assertEquals($result->stock, 1);
}

/**
 * @test
 * Das Produkt in der URL existiert nicht
 */
public function test_not_existing_product_cant_be_bought()
{
    $product = factory(Product::class)->create();

    $response = $this->get('/products/42/buy/15');
    $response->assertStatus(404);
}

/**
 * @test
 * Bei einem leeren Warenbestand kann das Produkt nicht gekauft werden
 */
public function test_product_with_empty_stock_cant_be_bought()
{
    $product = factory(Product::class)->create(['stock' => 0]);

    $response = $this->get('/products/1/buy/1');
    $response->assertStatus(403);
}

/**
 * @test
 * Es wurde keine Anzahl in der URL übergeben, obwohl mindestens ein Produkt gekauft werden muss
 */
public function test_product_cant_be_bought_without_quantity()
{
    $product = factory(Product::class)->create(['stock' => 15]);

    $response = $this->get('/products/1/buy/0');
    $response->assertStatus(403);
}

Nun hast Du über Unit Tests das Produkt Model und den Produkt Controller erfolgreich getestet und alle Code Pfade im Controller wurden mit den Tests „abgelaufen“. Als nächstes zeige ich Dir, wie Du einen Code Coverage Report zu Deinen PHPUnit Unit Tests im Laravel Projekt automatisch erstellen lassen kannst.

PHPUnit Code Coverage Report für Unit Testing in Laravel

Die Gründe Code Coverage Reports zu erzeugen sind vielfältig. Sie können für sich betrachtet werden um manuell zu prüfen, wie gut die Tesstabdeckung des eigenen Projektes ist. Ein weiterer Grund könnte die Integration in einen Build-Prozess darstellen.
PHPUnit liefert von Haus aus die Möglichkeit, Code Coverage Reports zu erzeugen. Das wird, wie alles andere auch, in der „phpunit.xml“ konfiguriert. Füge dazu am Ende der „phpunit.xml“ einfach folgenden Knoten an:

<logging>
   <log type="coverage-html" target="./report_code-coverage" charset="UTF-8"
        yui="true" highlight="true"
        lowUpperBound="50" highLowerBound="80" />
</logging>

Durch den „log“ Eintrag im „logging“ Knoten wird nach Abschluss der Tests eine HTML Struktur im Verzeichnis „/report_code-coverage“ erzeugt. Die Grenzen für schlecht und gut getesten Code lassen sich mit „lowUpperBound“ und „highLowerBound“ festlegen. Eine mehr Infos findest Du in der offiziellen Dokumentation von PHPUnit. Informationen zum Logging findest Du im Unterpunkt „Logging“ unter „PHPUnit – Configuration
Das Ergebnis Deines Produkt Controllers nach dem Ausführen der Tests sieht folgendermaßen aus:

Schritt eins im Code Coverage Report: Auswahl des Verzeichnis http

Schritt zwei im Code Coverage Report: Auswahl der Datei ProductController.php

Schritt drei im Code Coverage Report: Analyse der Datei ProductController.php
Du hast nun die grundlegenden Werkzeuge für das Unit Testing in Laravel 5.5 an der Hand. Du hast gelernt, wie Du Deine „phpunit.xml“ erweitern kannst, wie Du eine Testdatenbank mit SQLite3 einrichtest und wie Du Tests für Models und Controller erstellst. Zum Schluss hast Du noch die automatische Generierung von Code Coverage Reports aktiviert.

Mögliche Fehler beim Ausführen der PHPUnit Unit Tests in Laravel

Anbei folgt eine Liste möglicher Fehler die beim Unit Testing mit PHPUnit unter Laravel 5.5 auftreten können. Du kannst dabei helfen, diese Liste zu erweitern in dem Du von Deinen Fehlern berichtest.

Fehler 1 – Expected status code 200 but received 404

Beim Ausführen Deiner HTTP – Unit Tests (get / call) tritt folgender Fehler auf:

There was 1 failure:
1) TestsFeatureExampleTest::testBasicTest
Expected status code 200 but received 404.
Failed asserting that false is true.

Mögliche Lösung:

Du hast für Dein Laravel-Projekt einen „virtual host“ / „vhost“ Eintrag angelegt, damit Du Deine Website über eine „richte“ URL erreichen kannst: „http://mein-projekt.dev“.
In diesem Fall musst Du in der „phpunit.xml“ den folgenden Environment Eintrag ergänzen:

<env name="APP_URL" value="http://mein-projekt.dev"/>

 

Durch unsere langjährige Arbeit und über 100 erfolgreiche Projekte, konnten wir viele Erfahrungen sammeln. Dieses Know-How im Online-Marketing gaben wir u.a. bei Vorträgen von Google, der Industrie- und Handelskammer und der Handwerkskammer weiter.

Mit Know-How, Kreativität und Leidenschaft entwickeln wir auf unsere Kunden abgestimmte Marketing-Strategien, die Sie sicher und nachhaltig zum Erfolg führen. Gemeinsam setzen wir Ihr Online-Marketing so um, dass Sie langfristig Ihren Umsatz und Return-On-Investment steigern.

Jetzt kostenlosen Beratungstermin vereinbaren   oder unter 0561 / 850 194 76 anrufen.