Skip to content

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

KategorieSpeicherortAusführungBeschreibung
Unit-Teststests/Unit/npm run test:unitIsolierte Komponenten-Tests
Feature-Teststests/Feature/npm run test:featureFeature-Tests mit DB-Rollback
Modul-Testsmodules/*/*/tests/npm run test:modulesTests für Übungsmodule
Integration-Teststests/Integration/npm run test:integrationEchte API-Calls (manuell)
E2E-Teststests/E2E//e2e-test SkillBrowser-Automatisierung (manuell)
Legacy-Teststests/Legacy/-Alte PHPUnit-Tests (Archiv)

Test-Datenbank Setup

Initialisierung

Die Test-Datenbank wird automatisch beim ersten npm run test initialisiert.

Empfohlen (automatisch):

bash
# Intelligenter Test-Runner mit automatischem Setup
npm run test

Der neue test:run Command führt eine Smart-Check-Logik durch:

  1. Prüft ob Datenbank existiert
  2. Prüft ob Schema aktuell ist (Migrationen)
  3. Prüft ob Base-Data vorhanden ist
  4. Prüft ob Test-Accounts existieren
  5. Führt Setup nur aus, wenn einer der Checks fehlschlägt

Manuelles Setup:

bash
# 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:reset

Setup erzwingen (nach Schema-Änderungen):

bash
# Erzwingt frisches Setup, auch wenn Checks erfolgreich wären
npm run test:fresh

Siehe auch: TestRunCommand für detaillierte Dokumentation der Smart-Check-Logik

Base-Data User

Die Test-Datenbank enthält vorgefertigte User für verschiedene Rollen:

UsernameE-MailRolleVerwendung
bruce.waynebruce.wayne@gotham-academy.testadminAdministrative Tests
harvey.dentharvey.dent@gotham-academy.testeditorRedaktions-Tests
james.gordonjames.gordon@gotham-academy.testtesterQA-Tests
selina.kyleselina.kyle@gotham-academy.testteacherLehrer-Tests
jokerjoker@gotham-academy.testuserStandard-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.php

Unterscheidung 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:

php
// 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:

php
// 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:

FunktionBeschreibung
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

php
// 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

php
// 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

php
// Prüft ob ein Model einen gültigen Status hat
expect($model)->toHaveValidStatus(); // draft, pending, published, archived

API-Response-Expectations

php
// 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)

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)

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)

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

php
// 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
<?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

bash
# 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:setup

Smart-Check-Logik: Der npm run test Befehl prüft automatisch:

  1. Existiert die Test-Datenbank?
  2. Ist das Schema aktuell (alle Migrationen ausgeführt)?
  3. Sind Base-Data vorhanden (Glossarium, Reihen)?
  4. 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

bash
# 4. Nur Unit-Tests
npm run test:unit

# 5. Nur Feature-Tests
npm run test:feature

# 6. Nur Modul-Tests (parallel)
npm run test:modules

Gefilterte Test-Ausführung

bash
# 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-parallel

Hinweis: 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:

bash
# 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-failure

Verfügbare Optionen:

OptionBeschreibung
--freshDatenbank immer neu aufbauen (keine Smart-Checks)
--parallelTests parallel ausführen
--filter=Nur Tests ausführen die dem Filter entsprechen
--testsuite=Nur bestimmte Testsuite ausführen (Unit, Feature, Modules, Integration)
--coverageCode Coverage Report generieren
--stop-on-failureBei erstem Fehler abbrechen
--connection=testing_localDatenbank-Connection (Default: testing_local)

Klassische Artisan-Test-Befehle

bash
# Alle Tests (ohne Smart-Check, ohne Integration-Tests)
php artisan test

# Nur Hauptanwendung
php artisan test tests/

# Nur Module
php artisan test --testsuite=Modules

Integration-Tests ausführen

bash
# 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=Integration

Wichtig: Integration-Tests werden NICHT automatisch bei Standard-Testläufen ausgeführt, da sie echte API-Credits verbrauchen!

Spezifische Tests ausführen

bash
# 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

bash
# Mit NPM Script
npm run test:parallel

# Direkt mit Artisan
php artisan test --parallel --processes=4

# Automatische Prozessanzahl
php artisan test --parallel

Best Practices

Datasets für "zwei mal ist immer"

php
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:

php
// 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

php
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

php
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

php
// 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

php
// ALT: TestCase-Methoden (existieren nicht mehr)
$this->signInRoleAdmin();
$this->signInRoleEditor();

// NEU: Globale Pest-Funktionen
actingAsAdmin();
actingAsEditor();

3. DatabaseTransactions hinzufügen

php
// 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

php
// 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)

bash
npm run test              # Parallel mit Smart-Check
npm run test:fresh        # Parallel mit frischem Setup
npm run test:modules      # Nur Module, parallel

Vorteile:

  • 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

bash
npm run test:no-parallel  # Alle Tests sequentiell

Wann 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:

SuiteVerzeichnisseAusführung
Featuretests/Feature/--testsuite=Feature
Unittests/Unit/--testsuite=Unit
Modulesmodules/*/*/tests/--testsuite=Modules
Integrationtests/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-test Skill (startet parallele Browser-Instanzen)
  • Erstellung: /e2e-construct [beschreibung] Skill

Siehe: E2E-Testing Dokumentation