Leitlinien für Testing
Übersicht
Die Hermeneus-Test-Infrastruktur basiert auf Pest PHP und nutzt eine dedizierte Test-Datenbank mit Base-Data (vorbefüllte Testdaten). Die Infrastruktur bietet:
- Globale Helper-Funktionen für Authentifizierung
- Custom Expectations für domänenspezifische Assertions
- Wiederverwendbare Datasets für parametrisierte Tests
- Automatisches Datenbank-Rollback via DatabaseTransactions
- Parallele Testausführung für schnellere CI/CD-Pipelines
- E2E-Testing mit Browser-Automatisierung via Playwright MCP
Test-Kategorien im Überblick
| Kategorie | Speicherort | Ausführung | Beschreibung |
|---|---|---|---|
| Unit-Tests | tests/Unit/ | npm run test:unit | Isolierte Komponenten-Tests |
| Feature-Tests | tests/Feature/ | npm run test:feature | Feature-Tests mit DB-Rollback |
| Modul-Tests | modules/*/*/tests/ | npm run test:modules | Tests für Übungsmodule |
| Integration-Tests | tests/Integration/ | npm run test:integration | Echte API-Calls (manuell) |
| E2E-Tests | tests/E2E/ | /e2e-test Skill | Browser-Automatisierung (manuell) |
| Legacy-Tests | tests/Legacy/ | - | Alte PHPUnit-Tests (Archiv) |
Test-Datenbank Setup
Initialisierung
Die Test-Datenbank wird automatisch beim ersten npm run test initialisiert.
Empfohlen (automatisch):
# Intelligenter Test-Runner mit automatischem Setup
npm run testDer neue test:run Command führt eine Smart-Check-Logik durch:
- Prüft ob Datenbank existiert
- Prüft ob Schema aktuell ist (Migrationen)
- Prüft ob Base-Data vorhanden ist
- Prüft ob Test-Accounts existieren
- Führt Setup nur aus, wenn einer der Checks fehlschlägt
Manuelles Setup:
# Erstellt/Resettet die Test-Datenbank und importiert Base-Data
php artisan test:setup
# Oder über NPM
npm run test:setup
# Optional: Nur Base-Data neu importieren (schneller)
php artisan test:resetSetup erzwingen (nach Schema-Änderungen):
# Erzwingt frisches Setup, auch wenn Checks erfolgreich wären
npm run test:freshSiehe auch: TestRunCommand für detaillierte Dokumentation der Smart-Check-Logik
Base-Data User
Die Test-Datenbank enthält vorgefertigte User für verschiedene Rollen:
| Username | Rolle | Verwendung | |
|---|---|---|---|
| bruce.wayne | bruce.wayne@gotham-academy.test | admin | Administrative Tests |
| harvey.dent | harvey.dent@gotham-academy.test | editor | Redaktions-Tests |
| james.gordon | james.gordon@gotham-academy.test | tester | QA-Tests |
| selina.kyle | selina.kyle@gotham-academy.test | teacher | Lehrer-Tests |
| joker | joker@gotham-academy.test | user | Standard-User-Tests |
Passwort für alle Base-Data User: hermeneus#17
In Planung
- Artisan-Command, das ein SQLite-Abbild der Testing-Datenbank erstellt. (Reihentestung dann nur in SQLite)
- Artisan-Command, das personenbezogene Daten (Ausnahme: Entwickler-Accounts) in der Test-Datenbank obfuskiert.
Test-Struktur und Speicherorte
Hauptanwendung
- Feature-Tests:
/tests/Feature/[Feature]/*Test.php- Tests für Features der Hauptanwendung - Unit-Tests:
/tests/Unit/[Component]/*Test.php- Isolierte Tests für einzelne Komponenten - Integration-Tests:
/tests/Integration/[Service]/*Test.php- Tests mit echten externen API-Aufrufen - Legacy-Tests:
/tests/Legacy/*- Alte PHPUnit-Tests als Referenz
Module
- Modul-Feature-Tests:
/modules/[Kategorie]/[Modulname]/tests/Feature/*Test.php - Modul-Unit-Tests:
/modules/[Kategorie]/[Modulname]/tests/Unit/*Test.php - Modul-Integration-Tests:
/modules/[Kategorie]/[Modulname]/tests/Integration/*Test.php
Beispiel Modulstruktur:
modules/
├── Uebungen/
│ ├── UebungVorlage/
│ │ └── tests/
│ │ ├── Feature/
│ │ │ └── UebungVorlageAPITest.php
│ │ └── Unit/
│ │ └── UebungVorlageModelTest.php
│ └── UebungBedeutungenZuordnen/
│ └── tests/
│ └── Feature/
│ └── BedeutungenZuordnenTest.php
└── Services/
└── HermeneusAI/
└── tests/
├── Feature/
│ └── AIServiceTest.php
└── Integration/
└── AIServiceIntegrationTest.phpUnterscheidung der Test-Kategorien
Legacy-Tests
Alle Tests, die bisher völlig unprofessionell von mir in PHPUnit geschrieben wurden befinden sich im Ordner tests/Legacy. Sie können als Steinbruch für neue Tests verwendet werden.
Speicherort: tests/Legacy/
Feature-Tests
Tests, die ein Feature testen. Sie werden mit PestPHP geschrieben und nutzen gemockte externe Abhängigkeiten.
Speicherorte:
tests/Feature/modules/[Kategorie]/[Modulname]/tests/Feature/
Charakteristika:
- Testen Features aus Nutzersicht
- Verwenden Base-Data aus der Test-Datenbank
- Mocken externe API-Calls (keine echten Kosten)
- Automatisches DatabaseTransactions-Rollback
- Für Reihentestung in CI/CD vorgesehen
Unit-Tests bzw. Ad-hoc-Tests
Tests, die eine konkrete Methode, einen Bugfix, eine Klasse oder eine API-Route testen.
Speicherorte:
tests/Unit/modules/[Kategorie]/[Modulname]/tests/Unit/
Charakteristika:
- Isolierte Komponenten-Tests
- Schnelle Ausführung
- Keine automatischen DatabaseTransactions
- Für Einzeltestung vorgesehen
Integration-Tests
Tests, die echte Aufrufe an externe Dienste durchführen und deren tatsächliche Antworten validieren.
Speicherorte:
tests/Integration/modules/[Kategorie]/[Modulname]/tests/Integration/
Charakteristika:
- Echte API-Calls an externe Services (Anthropic Claude, OpenAI, etc.)
- Verbrauchen echte API-Credits
- Langsamere Ausführung
- NICHT automatisch in Standard-Testläufen enthalten
- Manuell ausführen oder gezielt in CI/CD triggern
Verwendungszwecke:
- Validierung von AI-Service-Integrationen (Tiro, Varro, Grammaticus, AeliusDonatus)
- Tests von Drittanbieter-APIs
- Prüfung echter Antwortformate und -qualität
- Regression-Tests nach API-Updates
Siehe auch: Integration-Tests für detaillierte Dokumentation
Pest-Konfiguration und globale Features
DatabaseTransactions
Feature-Tests nutzen automatisch DatabaseTransactions für sauberes Datenbank-Handling:
// tests/Pest.php
uses(Tests\TestCase::class, WithDatabaseTransactions::class)->in('Feature');Alle Datenbankänderungen in Feature-Tests werden nach jedem Test automatisch zurückgerollt.
Authentifizierung mit Base-Data Usern
Statt Factories verwenden Tests die vorbefüllten Base-Data User:
// ALT: Factory-basiert (veraltet)
$user = User::factory()->create();
$this->actingAs($user);
// NEU: Base-Data User
actingAsAdmin(); // bruce.wayne
actingAsEditor(); // harvey.dent
actingAsTester(); // james.gordon
actingAsTeacher(); // selina.kyle
actingAsUser(); // joker
// Oder generisch nach Rolle
actingAsRole('admin');
// Nur User holen ohne Anmeldung
$admin = getBaseDataUser('admin');Verfügbare Helper-Funktionen:
| Funktion | Beschreibung |
|---|---|
actingAsAdmin() | Meldet bruce.wayne (admin) an |
actingAsEditor() | Meldet harvey.dent (editor) an |
actingAsTester() | Meldet james.gordon (tester) an |
actingAsTeacher() | Meldet selina.kyle (teacher) an |
actingAsUser() | Meldet joker (user) an |
actingAsRole(string $role) | Meldet User anhand Rolle an |
getBaseDataUser(string $role) | Gibt User-Objekt ohne Anmeldung zurück |
Custom Expectations
Pest wurde mit domänenspezifischen Expectations erweitert:
Lemma/Vokabel-Expectations
// Prüft ob ein Lemma valide ist (lemma, bedeutung nicht leer)
expect($lemma)->toBeValidLemma();
// Prüft ob ein Lemma morphologisierbar ist
expect($lemma)->toBeMorphable();Übungs-Expectations
// Prüft ob eine Übung valide ist (base_uebung Relation existiert)
expect($uebung)->toBeValidUebung();
// Prüft ob content_data vorhanden ist
expect($uebung)->toHaveContentData();
// Prüft ob Gravitas-Level valide ist (0-100)
expect($uebung)->toHaveValidGravitas();Status-Expectations
// Prüft ob ein Model einen gültigen Status hat
expect($model)->toHaveValidStatus(); // draft, pending, published, archivedAPI-Response-Expectations
// Prüft auf erfolgreiche API-Response (200 + success:true)
expect($response)->toBeSuccessfulApiResponse();
// Prüft auf API-Fehler mit Status-Code
expect($response)->toHaveApiError(422); // Default: 422
expect($response)->toHaveApiError(404);Datasets für parametrisierte Tests
Wiederverwendbare Datasets befinden sich in tests/Datasets/:
User-Datasets (tests/Datasets/users.php)
// Alle Rollen
test('user kann Feature nutzen', function (string $role) {
actingAsRole($role);
// Test-Logic
})->with('roles'); // admin, editor, tester, teacher, user
// Nur erhöhte Rechte
test('nur privilegierte User', function (string $role) {
actingAsRole($role);
// Test-Logic
})->with('elevated_roles'); // admin, editor, tester
// Nur normale User
test('normale User', function (string $role) {
actingAsRole($role);
// Test-Logic
})->with('regular_roles'); // teacher, user
// Komplette User-Daten
test('user details', function (array $userData) {
$user = getBaseDataUser($userData['role']);
expect($user->email)->toBe($userData['email']);
})->with('base_data_users');Vokabel-Datasets (tests/Datasets/vocab.php)
// Wortarten
test('lemma für Wortart', function (string $wortart) {
// Test-Logic
})->with('wortarten'); // nomen, verb, adjektiv, pronomen, ...
// Nur morphologisierbare Wortarten
test('morphologie', function (string $wortart) {
// Test-Logic
})->with('morphable_wortarten'); // nomen, verb, adjektiv, pronomen, numerale
// Deklinationsklassen
test('deklination', function (array $dekl) {
expect($dekl['klasse'])->toBeIn(['a', 'o', 'kons', 'e', 'u', 'gem']);
})->with('deklinationsklassen');
// Konjugationsklassen
test('konjugation', function (array $konj) {
expect($konj['klasse'])->toBeIn(['a', 'e', 'kons', 'i', 'gem']);
})->with('konjugationsklassen');
// Grammatikalische Kategorien
->with('genera') // m, f, n
->with('kasus') // nom, gen, dat, akk, abl, vok
->with('numeri') // sg, plÜbungs-Datasets (tests/Datasets/uebungen.php)
// Alle registrierten Übungsmodule
test('übung kann erstellt werden', function (string $alias) {
// Test-Logic
})->with('uebung_aliases');
// Nur absolvierbare Übungen
test('übung absolvieren', function (string $alias) {
// Test-Logic
})->with('absolvable_uebungen');
// Nur evaluierbare Übungen
test('übung auswerten', function (string $alias) {
// Test-Logic
})->with('evaluable_uebungen');
// Nur Übungen mit Zeiterfassung
test('zeiterfassung', function (string $alias) {
// Test-Logic
})->with('timetrackable_uebungen');Übungs-Helper-Funktionen
// Alle registrierten Übungs-Aliase holen
$aliases = getUebungAliases();
// Registry-Eintrag für eine Übung
$entry = getRegistryEntry('uebung-bedeutungen-zuordnen');Leitlinien für Feature-Tests
- Konzeption: Feature-Tests sind für die Reihentestung und deshalb für die Ewigkeit gedacht.
- Test-Datei: Pro Feature eine Datei. Die Test-Datei ist nach dem zu testenden Feature benannt, z.B.
UserTest.php. - Möglichst oberflächlich testen: Achtung, oberflächlich heißt nicht vage, sondern so nah an der Nutzerinteraktion wie möglich. Der Test soll allein sicherstellen, dass das Feature funktioniert. Beispiel bei einem Übung-Feature: Gibt die API-Route die Daten und Datenstruktur zurück, die ich erwarte?
- Einmal ist keinmal, zwei mal ist immer: Teste das Feature mit zwei verschiedenen Testdaten. PestPHP bietet hierfür Datasets.
- Hinterlasse die Datenbank so wie du sie vorgefunden hast: Datenbankänderungen werden in Feature-Tests automatisch per DatabaseTransactions zurückgerollt - kein manuelles Cleanup mehr nötig!
Beispiel: Feature-Test mit neuer Infrastruktur
<?php
use function Pest\Laravel\{postJson, getJson};
test('admin kann neue übung erstellen', function () {
// Base-Data User nutzen
actingAsAdmin();
$response = postJson('/api/uebungen/uebung-bedeutungen-zuordnen/store', [
'title' => 'Test Übung',
'content_data' => ['vocab_ids' => [1, 2, 3]],
]);
// Custom Expectations verwenden
expect($response)->toBeSuccessfulApiResponse();
$uebung = Uebung::latest()->first();
expect($uebung)->toBeValidUebung()
->and($uebung)->toHaveContentData();
// Automatisches Rollback - kein manuelles Delete!
});
test('verschiedene rollen können übung erstellen', function (string $role) {
actingAsRole($role);
$response = postJson('/api/uebungen/uebung-bedeutungen-zuordnen/store', [
'title' => "Übung für {$role}",
'content_data' => ['vocab_ids' => [1, 2, 3]],
]);
expect($response)->toBeSuccessfulApiResponse();
})->with('elevated_roles'); // admin, editor, tester
test('alle übungstypen können erstellt werden', function (string $alias) {
actingAsAdmin();
$entry = getRegistryEntry($alias);
expect($entry)->not->toBeNull();
// Test-Logic für jeweiligen Typ
})->with('uebung_aliases');Test-Ausführung
NPM Scripts (Empfohlen)
Die Test-Infrastruktur bietet intelligente NPM-Scripts, die automatisch prüfen, ob die Test-Datenbank bereit ist und bei Bedarf ein Setup durchführen.
Grundlegende Test-Befehle
# 1. Alle Tests parallel ausführen (mit Smart-Check, ohne Integration-Tests)
npm run test
# 2. Alle Tests mit frischer Datenbank (erzwingt Setup)
npm run test:fresh
# 3. Nur Test-Datenbank aufsetzen (ohne Tests)
npm run test:setupSmart-Check-Logik: Der npm run test Befehl prüft automatisch:
- Existiert die Test-Datenbank?
- Ist das Schema aktuell (alle Migrationen ausgeführt)?
- Sind Base-Data vorhanden (Glossarium, Reihen)?
- Existieren die Test-Accounts (bruce.wayne, harvey.dent, etc.)?
Wenn alle Checks erfolgreich sind, werden die Tests direkt ausgeführt. Andernfalls wird automatisch ein Setup durchgeführt.
Spezifische Test-Suites
# 4. Nur Unit-Tests
npm run test:unit
# 5. Nur Feature-Tests
npm run test:feature
# 6. Nur Modul-Tests (parallel)
npm run test:modulesGefilterte Test-Ausführung
# 7. Tests mit Filter (z.B. nur "kann neue Übung erstellen")
npm run test:filter -- "kann neue Übung erstellen"
# 8. Alle Tests ohne Parallelisierung (für Debugging)
npm run test:no-parallelHinweis: Bei test:filter muss der Filter nach -- angegeben werden.
Direkte Artisan-Befehle
Der test:run Command
Der neue test:run Command kombiniert Setup-Checks und Test-Ausführung:
# Mit Smart-Check (Setup nur wenn nötig)
php artisan test:run --parallel
# Immer frisches Setup erzwingen
php artisan test:run --fresh --parallel
# Spezifische Testsuite
php artisan test:run --testsuite=Unit
# Mit Filter
php artisan test:run --filter="UebungBedeutungenZuordnen"
# Mit Coverage-Report
php artisan test:run --coverage --parallel
# Bei erstem Fehler abbrechen
php artisan test:run --stop-on-failureVerfügbare Optionen:
| Option | Beschreibung |
|---|---|
--fresh | Datenbank immer neu aufbauen (keine Smart-Checks) |
--parallel | Tests parallel ausführen |
--filter= | Nur Tests ausführen die dem Filter entsprechen |
--testsuite= | Nur bestimmte Testsuite ausführen (Unit, Feature, Modules, Integration) |
--coverage | Code Coverage Report generieren |
--stop-on-failure | Bei erstem Fehler abbrechen |
--connection=testing_local | Datenbank-Connection (Default: testing_local) |
Klassische Artisan-Test-Befehle
# Alle Tests (ohne Smart-Check, ohne Integration-Tests)
php artisan test
# Nur Hauptanwendung
php artisan test tests/
# Nur Module
php artisan test --testsuite=ModulesIntegration-Tests ausführen
# Nur Integration-Tests
php artisan test --testsuite=Integration
# Mit Group-Tag
php artisan test --group=integration
# Einzelner Integration-Test
php artisan test tests/Integration/AI/VarroIntegrationTest.php
# Mit test:run Command
php artisan test:run --testsuite=IntegrationWichtig: Integration-Tests werden NICHT automatisch bei Standard-Testläufen ausgeführt, da sie echte API-Credits verbrauchen!
Spezifische Tests ausführen
# Einzelnes Modul
php artisan test modules/Uebungen/UebungVorlage/tests/
# Einzelne Kategorie
php artisan test modules/Uebungen/*/tests/
# Einzelner Test
php artisan test modules/Uebungen/UebungVorlage/tests/Feature/UebungVorlageAPITest.php
# Mit Filter
php artisan test --filter="kann neue Übung erstellen"Parallel Testing für bessere Performance
# Mit NPM Script
npm run test:parallel
# Direkt mit Artisan
php artisan test --parallel --processes=4
# Automatische Prozessanzahl
php artisan test --parallelBest Practices
Datasets für "zwei mal ist immer"
it('validates input correctly', function ($input, $expected) {
$response = $this->post('/api/endpoint', $input);
expect($response->status())->toBe($expected);
})->with([
['valid input', ['name' => 'Test'], 200],
['invalid input', ['name' => ''], 422],
]);Module-Tests mit DatabaseTransactions
Für Modul-Tests gilt das gleiche Prinzip:
// modules/Uebungen/UebungVorlage/tests/Feature/UebungVorlageAPITest.php
use Tests\Support\WithDatabaseTransactions;
uses(WithDatabaseTransactions::class);
test('kann neue Übung vom Typ Vorlage erstellen', function () {
// Base-Data User statt Factory
actingAsEditor();
$response = postJson('/api/uebungen/uebung-vorlage/store', [
'title' => 'Test Übung',
'content' => 'Test Content'
]);
expect($response)->toBeSuccessfulApiResponse();
// Automatisches Rollback nach dem Test!
});Migration bestehender Tests
ALT: Factory-User + manuelles Cleanup
it('test something', function () {
$user = User::factory()->create();
$this->actingAs($user);
$model = Model::create([...]);
// Test logic
$model->delete(); // Manuelles Cleanup
$user->delete(); // Manuelles Cleanup
});NEU: Base-Data User + automatisches Rollback
uses(WithDatabaseTransactions::class); // Wenn nicht global definiert
test('test something', function () {
actingAsEditor(); // Base-Data User
$model = Model::create([...]);
// Test logic
// Automatisches Rollback - kein Delete nötig!
});Migration von Legacy-Tests
Legacy-Tests in tests/Legacy/ nutzen noch veraltete Patterns. Schritte zur Migration:
1. Syntax-Migration: PHPUnit → Pest
// ALT: PHPUnit
class UebungTest extends TestCase
{
public function test_kann_uebung_erstellen()
{
$this->assertTrue(true);
}
}
// NEU: Pest
test('kann uebung erstellen', function () {
expect(true)->toBeTrue();
});2. Authentifizierung aktualisieren
// ALT: TestCase-Methoden (existieren nicht mehr)
$this->signInRoleAdmin();
$this->signInRoleEditor();
// NEU: Globale Pest-Funktionen
actingAsAdmin();
actingAsEditor();3. DatabaseTransactions hinzufügen
// ALT: Kein Rollback
test('creates model', function () {
$model = Model::create([...]);
$model->delete(); // Manuell
});
// NEU: Automatisches Rollback
uses(WithDatabaseTransactions::class); // Falls nicht global
test('creates model', function () {
$model = Model::create([...]);
// Automatisches Rollback
});4. Expectations modernisieren
// ALT: PHPUnit-Assertions
$this->assertNotNull($lemma);
$this->assertNotEmpty($lemma->lemma);
$this->assertEquals(200, $response->status());
// NEU: Pest Expectations + Custom Expectations
expect($lemma)->toBeValidLemma();
expect($response)->toBeSuccessfulApiResponse();Parallele vs. Sequentielle Testausführung
Die Test-Infrastruktur unterstützt sowohl parallele als auch sequentielle Testausführung.
Parallele Ausführung (Standard)
npm run test # Parallel mit Smart-Check
npm run test:fresh # Parallel mit frischem Setup
npm run test:modules # Nur Module, parallelVorteile:
- 2-4x schnellere Ausführung
- Ideal für CI/CD-Pipelines
- Standard bei
npm run test
Wann nutzen:
- Reguläre Entwicklung
- CI/CD-Pipelines
- Nach Feature-Implementierung
Sequentielle Ausführung
npm run test:no-parallel # Alle Tests sequentiellWann nutzen:
- Debugging von Race Conditions
- Bei komplexen DB-Interaktionen
- Wenn parallele Tests flaky sind
phpunit.xml Test-Suites
Die phpunit.xml definiert vier Test-Suites:
| Suite | Verzeichnisse | Ausführung |
|---|---|---|
| Feature | tests/Feature/ | --testsuite=Feature |
| Unit | tests/Unit/ | --testsuite=Unit |
| Modules | modules/*/*/tests/ | --testsuite=Modules |
| Integration | tests/Integration/, modules/*/*/tests/Integration/ | --testsuite=Integration |
Hinweis: Integration-Tests sind standardmäßig ausgeschlossen (echte API-Credits).
E2E-Testing (Browser-Automatisierung)
Für End-to-End-Tests mit Browser-Automatisierung steht ein separates System zur Verfügung:
- User Flows: Markdown-Dateien in
tests/E2E/[Kategorie]/userflows/ - Ausführung:
/e2e-testSkill (startet parallele Browser-Instanzen) - Erstellung:
/e2e-construct [beschreibung]Skill
Siehe: E2E-Testing Dokumentation