Unit-Tests erstellen mit einem Zauberprompt
In modernen Symfony-Projekten sind automatisierte Unit-Tests unverzichtbar, um Code-Qualität und Wartbarkeit sicherzustellen. Doch das manuelle Schreiben von PHPUnit-Tests für jeden Controller ist zeitaufwändig und fehleranfällig. Genau hier setzt der Einsatz von Large Language Models (LLMs) wie ChatGPT an: Mit einem präzise formulierten „Magic Prompt“ lassen sich vollständige, lauffähige Testklassen automatisch generieren. In diesem Artikel zeige ich dir, wie du einen solchen Zauberprompt aufbaust, welche Vorteile er bietet und wie du ihn in deinen Entwicklungsprozess integrierst.
Warum ein Magic Prompt?
- Effizienzgewinn
Anstatt Boilerplate-Code für jeden Controller von Hand zu schreiben, erzeugt das LLM auf Knopfdruck eine Testklasse – inklusive aller Mock-Definitionen, Stub-Konfigurationen und Assertions. - Konsistenz & Best Practices
Ein zentral definierter Prompt stellt sicher, dass alle Tests den gleichen Stil, die gleichen Mock-Regeln und Symfony-spezifischen Vorgaben befolgen. - Onboarding
Neue Teammitglieder verstehen den Testaufbau schneller, wenn er automatisch und nachvollziehbar aus demselben Prompt entsteht.
Aufbau des Zauberprompts
Ein hochwertiger Prompt muss alle Details enthalten, die für saubere PHPUnit-Tests nötig sind. Im Zentrum stehen:
- Konstruktor-Injektion aller Abhängigkeiten (statt Reflection-Hacks).
- Echte Instanzen für finale Klassen (z. B. RateLimiterFactory, Envelope).
- Mocks nur für Interfaces und Services (Repository, Validator, MessageBusInterface etc.).
- Exakte Methodensignaturen von Symfony-Hilfsmethoden (
isGranted,getUser,json). - Verwendung von ConstraintViolationList statt einfacher Arrays.
- PHPDoc-Annotationen für alle Mock-Properties und Inline-Variablen.
- Callback-Stubbing für ParameterBagInterface mit Default-Werten.
Durch die Kombination dieser Regeln im Prompt generiert das LLM direkt eine PHPUnit-Testklasse, die ohne zusätzliche Hacks und ohne Kernel-Bootstrapping per vendor/bin/phpunit läuft.
Detaillierte Erläuterung der Prompt-Punkte
| Punkt | Zweck |
|---|---|
| 1. Keine Reflection-Hacks | Schützt readonly-Properties und hält Tests Symfony-kompatibel. |
| 2. Echte Instanzen für finale Klassen | Vermeidet Unmockable-Errors und Seiteneffekte. |
| 3. Nur Interfaces und Services mocken | Sorgt für klare Testgrenzen und bessere Lesbarkeit. |
| 4. Exakte Methodensignaturen | Stellt Kompatibilität mit allen Symfony-Versionen sicher. |
| 5. Stub-Überschreibungen in Test-Subclass | Erlaubt kontrollierte Überschreibung von Controller-Hilfsmethoden. |
| 6. ConstraintViolationList zurückgeben | Verhindert Typfehler bei Validator-Stubs in Symfony 6+. |
| 7. PHPDoc für Mock-Properties | Unterdrückt IDE-Warnings (Intelliphense etc.). |
| 8. Inline-PHPDoc für lokale Mocks | Gleicher Effekt auf Methodenscope-Ebene. |
| 9. Genauer Rückgabetyp bei Repositories | EntityRepository statt nur ObjectRepository, für spezifische Methoden. |
| 10. flush() ohne willReturn() | Da flush(): void keinen Rückgabewert hat. |
| 11. dispatch() gibt echten Envelope | Testet echtes Messenger-Verhalten. |
| 12. JSON-Antworten via json_decode() prüfen | Robuster gegen Format- und Whitespace-Änderungen. |
| 13. Läuft ohne Kernel-Bootstrap | Schneller, leichter Unit-Test ohne Integrationstest-Overhead. |
| 14. Callback-Stub für ParameterBagInterface | Default-Werte werden korrekt zurückgegeben, keine UnexpectedValueException. |
Integration in den Entwicklungsprozess
- Prompt zentral verwalten
Lege deinen Zauberprompt in einem Repository ab (z. B.docs/prompt.md). - Automatisierte Generierung
Baue einen kleinen CLI-Befehl, der per Git-Hook oder Makefile den Prompt an ChatGPT sendet und das Ergebnis intests/Controllerablegt. - Code-Review
Auch automatisch generierte Tests sollten per Pull Request geprüft werden – so schaffst du zusätzliche Sicherheit. - Prompt-Versionierung
Ändert sich dein Symfony-Standard (z. B. neue Signaturen), versioniere den Prompt und weise im Projekt auf die korrekte Version hin.
Best Practices & Lessons Learned
- Keep it DRY: Verwende Variablen und Templates, falls dein Prompt für mehrere Controller-Typen gilt.
- Regelmäßig aktualisieren: Nach jedem größeren Symfony-Release prüfst du deinen Prompt auf neue Methodensignaturen oder Dependency-Änderungen.
- Granulare Kontrolle: Bei komplexen Logiken in Controllern erwäge zusätzliche manuelle Tests oder gezielte Integrationstests.
- Monitoring: Verfolge, wie oft generierte Tests angepasst werden – hohes Änderungsvolumen kann auf Lücken im Prompt hinweisen.
Der Zauberprompt
**Du bekommst eine Symfony-Controller-Klasse (z. B. `ApiSomethingController`). Erstelle mir eine funktionsfähige PHPUnit-Testklasse, die sämtliche Abhängigkeiten per Konstruktor injiziert und dabei folgende Vorgaben erfüllt — inklusive aller Lessons Learned, damit künftige Tests ohne Inkompatibilitäten durchlaufen:**
1. **Keine** `readonly`-Properties per Reflection ändern oder hacken.
2. **Keine** finalen Klassen mocken (z. B. `RateLimiterFactory`, `Envelope`); instanziiere sie stattdessen real (z. B. `new RateLimiterFactory(..., new InMemoryStorage())`, `new Envelope(new Send…Message(...))`).
3. Nur reguläre Mock-Objekte für Interfaces und Services (Repository, Validator, PasswordHasher, RateLimitService, UserDeletionService, MessageBusInterface etc.) verwenden.
4. **Methodensignaturen exakt übernehmen**:
* `public function isGranted(string $attribute, $subject = null): bool`
* `protected function getUser(): ?UserInterface`
* `public function json(mixed $data, int $status = 200, array $headers = [], array $context = []): JsonResponse`
5. `getUser()`, `isGranted()` und `json()` in der anonymen Test-Controller-Subclass nur via PHPUnit-Stub überschreiben und Signaturen 1:1 matchen.
6. `ValidatorInterface::validate()` so stubben, dass es ein echtes `ConstraintViolationList`-Objekt liefert, nicht ein Array.
7. **Für alle** gemockten **Klassen-Properties** **nicht** typisieren, sondern **ausschließlich** per PHPDoc annotieren, z. B.
```php
/** @var UserRepository&\PHPUnit\Framework\MockObject\MockObject */
private $userRepository;
```
damit Intelliphense keine „Found MockObject”-Warnings wirft.
8. **Für alle lokalen Mock-Variablen in jeder Testmethode** ebenfalls eine Inline-PHPDoc hinzufügen, z. B.
```php
/** @var UserPasswordHasherInterface&\PHPUnit\Framework\MockObject\MockObject $hasher */
$hasher = $this->createMock(UserPasswordHasherInterface::class);
```
9. **Wichtig:** Wenn Du `$entityManager->getRepository(User::class)` mockst, musst Du **genau** den Rückgabetyp `Doctrine\ORM\EntityRepository` statt nur `ObjectRepository` angeben!
10. **`flush()`** hat Rückgabetyp `void`. Stubpe es **ohne** `willReturn()`, also z. B.
```php
$em->expects($this->once())->method('flush');
```
11. **MessageBusInterface::dispatch()** soll einen echten `Envelope` zurückliefern. Instanziiere ihn real:
```php
->willReturn(new Envelope(new SendAccountDeletedMailMessage(...)));
```
12. Teste JSON-Antworten immer über
```php
$data = json_decode($response->getContent(), true);
$this->assertSame([...], $data);
```
statt String-Vergleichen.
13. Direkt lauffähig mit `phpunit`, **ohne** zusätzliche Hacks oder Kernel-Bootstrapping.
14. **Stub ParameterBagInterface::get() korrekt:** Wenn Du `$params->get(...)` stubbst, nutze eine Callback-Stub mit **zwei Parametern**
```php
$params->method('get')->willReturnCallback(function(string $key, $default = null) {
$map = ['foo' => 'bar'];
return $map[$key] ?? $default;
});
```
damit der Default-Wert für ungetestete Keys zurückgegeben wird und keine `UnexpectedValueException` geworfen wird.
**Gib nur die vollständige Testklasse in PHP zurück.**
0 Kommentare