Laravel-Tests zu schreiben ist die eine Gewohnheit, die ein Projekt — einen Haufen Code, der nur „zu funktionieren scheint“ — in eine Codebasis verwandelt, die du mit Zuversicht ändern kannst. Tests sagen dir sofort, welches Verhalten ein Refactoring kaputt gemacht hat; sie dienen als lebendige Dokumentation für die nächste Entwicklerin; und vor allem nehmen sie jedem Deployment die „Habe ich etwas zerstört?“-Anspannung. In diesem Leitfaden behandeln wir Laravels zwei Test-Engines — das standardmäßige PHPUnit und das immer beliebtere Pest mit seiner schlankeren Syntax — anhand praktischer Beispiele.
Wie das Testen in Laravel aufgebaut ist
Ein frisches Laravel-Projekt wird bereits mit Tests ausgeliefert. Die Datei phpunit.xml im Wurzelverzeichnis definiert die Testumgebung, während sich der Ordner tests/ in zwei Verzeichnisse aufteilt: tests/Unit und tests/Feature. Die Unterscheidung ist konzeptionell:
- Unit-Tests prüfen eine einzelne Klasse oder Methode isoliert, ohne das gesamte Framework hochzufahren. Sie sind schnell und konzentrieren sich auf reine Logik (Berechnungen, Formatierung, kleine Services).
- Feature-Tests verifizieren eine HTTP-Anfrage, eine Route, die Datenbank und mehrere zusammenarbeitende Klassen von Anfang bis Ende. Diese liefern in der Praxis meist den größten Wert.
Am praktischsten führst du sie mit Laravels eigenem Wrapper aus:
php artisan test
# oder direkt:
./vendor/bin/phpunit
# um einen einzelnen Test zu filtern:
php artisan test --filter=ProjectCanBeCreated
In phpunit.xml wird die Testumgebung typischerweise mit APP_ENV=testing und einer In-Memory-SQLite-Datenbank (DB_CONNECTION=sqlite, DB_DATABASE=:memory:) konfiguriert. So laufen Tests jedes Mal auf einem sauberen Schema, ohne deine echte Datenbank anzufassen.
Den ersten Feature-Test schreiben
Artisan genügt, um eine Testklasse zu erzeugen:
php artisan make:test ProjectTest # tests/Feature
php artisan make:test PriceTest --unit # tests/Unit
Ein einfacher Feature-Test beginnt damit, zu bestätigen, dass eine Seite korrekt lädt. Mit PHPUnit-Syntax:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ProjectTest extends TestCase
{
use RefreshDatabase;
public function test_homepage_loads_successfully(): void
{
$response = $this->get('/');
$response->assertStatus(200);
$response->assertSee('Aslain');
}
}
Hier simuliert $this->get('/') eine echte HTTP-Anfrage; assertStatus und assertSee verifizieren die Antwort. Der Name der Testmethode muss mit test_ beginnen (oder mit dem Attribut #[Test] markiert sein), damit PHPUnit ihn erkennt.
Datenbanktests und RefreshDatabase
Bei Tests, die die Datenbank berühren, ist der RefreshDatabase-Trait entscheidend: Er setzt das Schema vor jedem Test innerhalb einer Transaktion zurück und macht sie danach rückgängig, sodass sich Tests nie gegenseitig beeinflussen. Zum Erzeugen von Daten verwendet man Factories:
public function test_project_is_persisted(): void
{
$project = Project::factory()->create([
'title' => ['de' => 'Testprojekt'],
]);
$this->assertDatabaseHas('projects', [
'id' => $project->id,
]);
}
assertDatabaseHas fragt eine Zeile direkt ab. Sein Gegenstück assertDatabaseMissing ist praktisch beim Testen von Löschvorgängen. Factories liegen unter database/factories und erzeugen über den fake()-Helfer realistische Zufallsdaten.
Authentifizierung, JSON und Formularübermittlung
In echten Anwendungen erfordern die meisten Routes einen angemeldeten Benutzer. Laravel erledigt das in einer Zeile mit actingAs:
public function test_authorised_user_can_add_project(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/admin/projects', [
'title_de' => 'Neues Projekt',
]);
$response->assertRedirect('/admin/projects');
$this->assertDatabaseHas('projects', ['title->de' => 'Neues Projekt']);
}
Für JSON-basierte APIs gibt es Methoden wie getJson und postJson sowie strukturbewusste Assertions wie assertJson / assertJsonStructure:
$this->getJson('/api/projects')
->assertOk()
->assertJsonCount(3, 'data')
->assertJsonStructure(['data' => [['id', 'title']]]);
Dieselben Tests mit Pest kompakter schreiben
Pest ist ein Test-Framework, das auf PHPUnit aufbaut; die Engine ist identisch, die Syntax flüssiger. Statt Klassen und Methoden schreibst du funktionsbasierte Tests. Derselbe Feature-Test sieht in Pest so aus:
<?php
use App\Models\User;
it('lässt einen berechtigten Benutzer ein Projekt anlegen', function () {
$user = User::factory()->create();
$this->actingAs($user)
->post('/admin/projects', ['title_de' => 'Neues Projekt'])
->assertRedirect('/admin/projects');
expect(Project::count())->toBe(1);
});
Der expect()-Ausdruck bietet eine lesbare Assertion-Kette (toBe, toBeTrue, toContain…). Traits wie RefreshDatabase wendet man über uses() in der Datei tests/Pest.php auf ein ganzes Verzeichnis auf einmal an. Um Pest einzurichten, fügst du das Paket pestphp/pest mit Composer hinzu und führst php artisan pest:install aus; deine bestehenden PHPUnit-Tests laufen unverändert weiter daneben.
Abhängigkeiten nachbilden: Mocks und Fakes
Gute Tests verbinden sich nie wirklich mit externen Diensten (E-Mail, Zahlung, Benachrichtigungen). Laravel liefert dafür fertige Fakes. Du kannst zum Beispiel bestätigen, dass eine Benachrichtigung gesendet wurde, ohne eine echte E-Mail zu verschicken:
use Illuminate\Support\Facades\Mail;
Mail::fake();
// ... Aktion ...
Mail::assertSent(WelcomeMail::class);
Dieselbe Idee gilt für Queue::fake(), Event::fake(), Storage::fake() und Http::fake(). Sie halten Tests schnell und deterministisch — sie hängen nicht mehr von einer Netzwerkverbindung oder dem Zustand eines Drittanbieterdienstes ab.
Häufige Fragen
Soll ich PHPUnit oder Pest wählen?
Beide nutzen dieselbe Engine, es ist also kein Wettbewerb technischer Überlegenheit. Möchtest du eine klassenbasierte, vertraute und unternehmensfreundliche Struktur, wähle PHPUnit; bevorzugst du weniger Boilerplate und lesbare Syntax, ist Pest sinnvoll. Für neue Projekte wird Pest zunehmend die Standardwahl, doch die Gewohnheiten deines Teams sollten entscheiden.
Soll ich alles testen?
Nein. 100% Abdeckung anzustreben ist meist verschwenderisch. Teste zuerst die geschäftskritischen Abläufe (Zahlung, Registrierung, Autorisierung, Bestellungen) und die Stellen, die früher schon kaputtgegangen sind. Tests für einfache Getter/Setter oder den Code des Frameworks selbst zu schreiben ist vergebene Mühe.
Warum sind meine Tests langsam?
Die häufigsten Ursachen sind der Zugriff auf eine echte Datenbank statt In-Memory-SQLite und das Nicht-Faken externer Dienste. Mit :memory:-SQLite, RefreshDatabase und passenden Fakes kann eine Testsuite in Sekunden laufen.
Möchtest du Tests fest in deinem Projekt verankern? Wenn du Hilfe brauchst, eine zuverlässige Testsuite auf deiner bestehenden Laravel-App einzurichten, bis hin zur CI/CD-Integration, melde dich bei mir — lass uns deine Codebasis gemeinsam härten.