statische Daten oder parametrisierte Generierung?
Fixtures und Factories lösen dasselbe Problem auf unterschiedliche Weise: vorhersehbare, feste Testdaten gegen flexible, zur Laufzeit generierte Datensätze. Dieser Artikel zeigt, wann welcher Ansatz die bessere Wahl ist, wie Factories ohne Overengineering auskommen, und wie ein hybrides Modell aus beiden Ansätzen stabile und lesbare Tests für Kundendaten und Produktdaten ermöglicht.
Inhaltsverzeichnis
- 1. Zwei Philosophien: feste Fixtures gegen generierte Factories
- 2. Wann statische Fixtures die bessere Wahl sind
- 3. Wann Factories die bessere Wahl sind
- 4. Lesbarkeit gegen Flexibilität
- 5. Factory-Overengineering vermeiden
- 6. Praxisbeispiel: eine Factory für den Testkunden
- 7. Praxisbeispiel: ein Testprodukt als Fixture mit Varianten-Factory
- 8. Der hybride Ansatz: 80 Prozent Fixtures, 20 Prozent Factories
- 9. Fixtures und Factories im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Zwei Philosophien: feste Fixtures gegen generierte Factories
Wer Testdaten für eine E2E-Suite aufbaut, trifft implizit oder explizit eine Grundsatzentscheidung zwischen zwei Philosophien. Statische Fixtures sind fest definierte Datenstrukturen, meist als JSON- oder TypeScript-Objekt im Repository abgelegt, die bei jedem Testlauf exakt dieselben Werte liefern. Factories hingegen sind Funktionen, die Testdaten erst zur Laufzeit erzeugen, mit sinnvollen Standardwerten und der Möglichkeit, einzelne Felder gezielt zu überschreiben. Beide Ansätze lösen dasselbe Grundproblem, nämlich ein Testobjekt mit definiertem Zustand bereitzustellen, aber mit fundamental unterschiedlichen Kompromissen bei Vorhersehbarkeit, Flexibilität und Wartungsaufwand.
Die Wahl zwischen Fixture und Factory ist keine reine Geschmacksfrage, sondern eine Architekturentscheidung, die sich über die gesamte Lebensdauer der Testsuite auswirkt. Ein Team, das ausschließlich auf Fixtures setzt, sammelt mit der Zeit dutzende fast identische Dateien für jede Variation eines Testfalls an. Ein Team, das ausschließlich auf Factories setzt, riskiert unlesbare Testsetups, in denen die tatsächlich relevanten Werte eines Tests hinter Abstraktionsebenen verschwinden. Die folgenden Abschnitte ordnen ein, wann welcher Ansatz überwiegt, und zeigen ein praktisches Beispiel für einen Testkunden und ein Testprodukt.
2. Wann statische Fixtures die bessere Wahl sind
Statische Fixtures spielen ihre Stärken bei kleinen, stabilen Datensätzen aus, die sich selten ändern und deren exakter Inhalt für den Test selbst relevant ist. Ein Test, der prüft, ob eine bestimmte Postleitzahl korrekt formatiert oder ein Sonderzeichen im Nachnamen korrekt escaped wird, profitiert von einer Fixture mit exakt diesem einen kuriosen Wert. Weil Fixtures beim Testlauf nicht erst berechnet werden müssen, sind sie außerdem schneller: Es entfällt jeder Overhead durch Zufallsgeneratoren, Parameterauflösung oder Objektkomposition.
Der größte Vorteil liegt jedoch in der Nachvollziehbarkeit. Wer eine Fixture-Datei öffnet, sieht sofort und vollständig, mit welchen Daten der Test arbeitet, ohne den Aufruf einer Factory-Funktion mit mehreren Overrides gedanklich auflösen zu müssen. Für das Onboarding neuer Teammitglieder und für Code-Reviews ist dieser direkte Bezug zwischen Datei und Wert ein erheblicher Vorteil. Fixtures eignen sich daher besonders für Referenzdaten, Randfälle mit exotischen Zeichen oder Formaten, und für Tests, die exakt reproduzierbare Snapshots erwarten.
{
"customer": {
"id": "cust_fixture_edge_001",
"email": "edge.case.customer@example.test",
"firstname": "Rene",
"lastname": "O'Connor-Fuentes",
"address": {
"street": "Teststrasse 12",
"postcode": "80331",
"city": "Muenchen",
"country": "DE"
},
"note": "Fixture fuer Escaping und Sonderzeichen-Tests im Nachnamen, Wert bewusst konstant gehalten"
}
}
3. Wann Factories die bessere Wahl sind
Factories entfalten ihren Wert, sobald ein Test Variation braucht, die mit einer endlichen Anzahl Fixture-Dateien nicht praktikabel abzubilden ist. Ein Checkout-Test, der zwanzig verschiedene Kombinationen aus Zahlungsart, Versandland und Rabattcode durchspielt, würde mit zwanzig separaten Fixture-Dateien schnell unübersichtlich. Eine einzige Factory mit Parametern für Zahlungsart, Land und Rabattcode deckt dieselbe Variationsbreite mit einer Funktion und klar benannten Aufrufen ab.
Der zweite, oft unterschätzte Vorteil betrifft parallele Testläufe. Zwei Tests, die dieselbe statische Fixture mit derselben E-Mail-Adresse verwenden, kollidieren, sobald sie gleichzeitig gegen dieselbe Umgebung laufen. Eine Factory kann bei jedem Aufruf einen eindeutigen Bezeichner generieren, etwa über einen Zufallswert oder einen Zeitstempel, und verhindert damit strukturell, dass parallele Worker sich gegenseitig überschreiben. Für Randfälle wie gesperrte Konten, abgelaufene Gutscheine oder Produkte mit Bestand null ist eine parametrisierte Factory zudem der einzige praktikable Weg, diese Zustände gezielt und ohne separate Datei pro Fall zu erzeugen.
4. Lesbarkeit gegen Flexibilität
Die Flexibilität von Factories hat einen Preis, der sich erst mit wachsender Suite bemerkbar macht: Lesbarkeit. Ein Test, der createCustomer({ isLocked: true }) aufruft, verrät auf den ersten Blick nicht, welche der zwanzig übrigen Felder die Factory mit welchen Standardwerten befüllt. Wer verstehen will, ob die Adresse in Deutschland oder in einem anderen Land liegt, muss die Factory-Implementierung öffnen und die Default-Werte nachschlagen. Bei einer Fixture-Datei entfällt dieser Schritt vollständig, weil jeder Wert direkt sichtbar im Test-Setup steht.
Dieses Problem verschärft sich, wenn Factories selbst wieder andere Factories aufrufen, etwa eine Bestell-Factory, die intern eine Kunden-Factory und eine Produkt-Factory kombiniert. Ein Testfehler in einem solchen Setup erfordert dann mehrere Ebenen an Debugging, bevor klar ist, welcher der drei verschachtelten Aufrufe den unerwarteten Wert erzeugt hat. Die pragmatische Regel lautet, Overrides im Testcode selbst so vollständig zu benennen, dass der für den jeweiligen Test relevante Zustand direkt aus dem Aufruf lesbar ist, statt sich auf implizite Defaults zu verlassen, die anderswo im Projekt verstreut liegen.
5. Factory-Overengineering vermeiden
Factories laden dazu ein, immer mehr Flexibilität einzubauen, bis die Abstraktion selbst zum Wartungsproblem wird. Ein typisches Warnsignal ist die Builder-Chain, bei der eine Factory über .withAddress().withLockedAccount().withGroup(3).build() konfiguriert wird. Jede zusätzliche with-Methode erhöht die Testoberfläche der Factory selbst und verlangt eigene Tests, eigene Dokumentation und eigene Wartung, obwohl in der Praxis oft nur zwei oder drei Kombinationen tatsächlich benutzt werden.
Ähnlich riskant sind Trait- oder Plugin-Systeme, die versuchen, jede denkbare Zukunftskombination vorab abzubilden. Das Ergebnis ist häufig eine Factory mit fünfzig Konfigurationsoptionen, von denen die Testsuite nur fünf tatsächlich nutzt. Das YAGNI-Prinzip gilt für Testdaten genauso wie für Produktivcode: Eine Factory sollte mit den Overrides beginnen, die aktuell existierende Tests brauchen, und nur dann wachsen, wenn ein neuer Test einen echten, konkreten Bedarf hat. Eine einfache Funktion mit einem flachen Overrides-Objekt ist fast immer wartbarer als eine generische, konfigurierbare Builder-Hierarchie, die niemand vollständig überblickt.
// ANTI-PATTERN: an overengineered builder chain for customer test data.
// Twelve "with" methods exist, but most tests only ever use two of them.
class CustomerBuilder {
private data: Record<string, unknown> = {
email: 'default@example.test',
isLocked: false,
groupId: 1,
};
withAddress(address: unknown): this { this.data.address = address; return this; }
withLockedAccount(): this { this.data.isLocked = true; return this; }
withGroup(id: number): this { this.data.groupId = id; return this; }
withNewsletterOptIn(): this { this.data.newsletter = true; return this; }
withLoyaltyTier(tier: string): this { this.data.loyaltyTier = tier; return this; }
// ... seven more "with" methods, each used in exactly one test
build(): Record<string, unknown> { return this.data; }
}
// Reading this call requires opening the builder class to know what
// "withGroup(3)" actually sets on the resulting customer object.
const customer = new CustomerBuilder()
.withGroup(3)
.withLockedAccount()
.build();
// BETTER: a flat factory function with named overrides, see section 6 below.
6. Praxisbeispiel: eine Factory für den Testkunden
Eine gute Kunden-Factory beginnt mit sinnvollen Standardwerten für einen typischen, gültigen Kunden und erlaubt punktuelle Overrides für die Felder, die ein konkreter Test tatsächlich variieren muss. Wichtig ist, dass die Factory eine eindeutige E-Mail-Adresse generiert, etwa über eine Zufallsbibliothek wie Faker, damit parallele Testläufe sich nicht gegenseitig blockieren. Overrides sollten als flaches, optionales Objekt übergeben werden, nicht als verschachtelte Konfiguration, damit der Aufruf im Test selbst lesbar bleibt.
Das folgende Beispiel zeigt eine TypeScript-Factory, die einen Testkunden über die Anwendungs-API anlegt. Die Standardwerte decken den häufigsten Fall ab, einen aktiven, nicht gesperrten Kunden mit gültiger Adresse. Für Randfälle wie ein gesperrtes Konto genügt ein einziges Override-Feld, ohne dass ein Test die übrigen zwanzig Felder kennen oder wiederholen muss.
// factories/customer.factory.ts
import { faker } from '@faker-js/faker';
import { apiClient } from '../support/api-client';
interface CustomerOverrides {
email?: string;
isLocked?: boolean;
groupId?: number;
country?: string;
}
/**
* Creates a customer via the application API with sensible defaults.
* Overrides let individual tests target specific edge cases without
* having to repeat every unrelated field.
*/
export async function createCustomer(overrides: CustomerOverrides = {}) {
const payload = {
// Unique email per call keeps parallel test workers collision free
email: overrides.email ?? `customer.${faker.string.uuid()}@example.test`,
firstname: faker.person.firstName(),
lastname: faker.person.lastName(),
groupId: overrides.groupId ?? 1,
isLocked: overrides.isLocked ?? false,
country: overrides.country ?? 'DE',
};
const response = await apiClient.post('/customers', payload);
return response.data; // includes generated id, used later for cleanup
}
7. Praxisbeispiel: ein Testprodukt als Fixture mit Varianten-Factory
Für ein Testprodukt eignet sich häufig der umgekehrte Ansatz: eine statische Fixture für das Standardprodukt, das in den meisten Tests unverändert verwendet wird, und eine ergänzende Factory für die Fälle, in denen ein Test gezielt eine Variante braucht, etwa unterschiedliche Bestände, Preise oder Sonderzeichen im Produktnamen. Das Standardprodukt ändert sich selten, deshalb lohnt sich für dieses eine Objekt der Determinismus einer Fixture-Datei mehr als die Flexibilität einer Factory.
Für Bestandstests, Preistests oder Lokalisierungstests reicht die statische Fixture jedoch nicht aus, weil jeder dieser Tests einen anderen Zustand des Produkts braucht. Eine schlanke Produkt-Factory, die auf denselben Basiswerten wie die Fixture aufbaut, aber gezielte Overrides für Bestand, Preis oder Name erlaubt, deckt diese Fälle ab, ohne für jede Kombination eine eigene Datei anzulegen. Die beiden Ansätze schließen sich nicht aus, sondern ergänzen sich für dasselbe Produkt-Entity.
// fixtures/test-product.fixture.ts
// Stable base product, used unmodified by the majority of tests.
export const TEST_PRODUCT_FIXTURE = {
sku: 'TEST-PRODUCT-BASE',
name: 'Fixture Testprodukt',
price: 49.90,
qty: 100,
status: 1,
} as const;
// factories/product.factory.ts
// Builds edge case variants on top of the same stable base fixture.
interface ProductVariantOverrides {
qty?: number;
price?: number;
name?: string;
}
export function createProductVariant(overrides: ProductVariantOverrides = {}) {
return {
...TEST_PRODUCT_FIXTURE,
// Unique SKU per call avoids collisions between parallel test runs
sku: `TEST-PRODUCT-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
...overrides,
};
}
// Edge cases built on the same stable base, no separate file per case
const outOfStock = createProductVariant({ qty: 0 });
const heavilyDiscounted = createProductVariant({ price: 0.01 });
8. Der hybride Ansatz: 80 Prozent Fixtures, 20 Prozent Factories
In der Praxis bewährt sich fast immer eine Kombination statt eine dogmatische Festlegung auf einen der beiden Ansätze. Die Faustregel: Fixtures für den Teil der Testdaten, der über die gesamte Suite hinweg stabil bleibt und selten Variation braucht, Factories für den Teil, der von Test zu Test unterschiedliche Zustände erfordert. In der Praxis liegt dieses Verhältnis oft nahe achtzig zu zwanzig, daher der Name des Musters.
Konkret bedeutet das: Ein Referenzkatalog mit Standardprodukten, feste Konfigurationswerte und Randfälle mit exotischen Zeichen bleiben Fixtures. Kernentitäten, die für jeden Test unterschiedlich variiert werden müssen, etwa Kunden mit unterschiedlichem Kontostatus oder Bestellungen mit unterschiedlichen Positionen, werden über Factories erzeugt. Dieses hybride Muster hält die Anzahl der Fixture-Dateien klein und überschaubar, während Factories nur dort eingesetzt werden, wo ihre Flexibilität tatsächlich gebraucht wird, statt sie reflexartig für jede Testdatensituation zu verwenden.
// e2e/checkout.spec.ts
// Hybrid usage: fixture for the stable product, factory for the variable customer state
import { test, expect } from '@playwright/test';
import { TEST_PRODUCT_FIXTURE } from '../fixtures/test-product.fixture';
import { createCustomer } from '../factories/customer.factory';
test('locked customer cannot complete checkout', async ({ page }) => {
// Stable reference product: the fixture is enough, no factory needed
const product = TEST_PRODUCT_FIXTURE;
// Variable customer state: factory generates a unique, locked account
const customer = await createCustomer({ isLocked: true });
await page.goto(`/product/${product.sku}`);
await page.getByRole('button', { name: 'Add to cart' }).click();
await page.goto('/checkout');
await page.getByLabel('Email').fill(customer.email);
await expect(page.getByText('Account is locked')).toBeVisible();
});
9. Fixtures und Factories im direkten Vergleich
Beide Ansätze haben ihren berechtigten Platz in einer E2E-Suite, aber die Wahl hat direkte Konsequenzen für Lesbarkeit, Setupgeschwindigkeit und Parallelisierbarkeit. Die folgende Übersicht vergleicht beide Strategien entlang der wichtigsten Entscheidungsdimensionen.
| Dimension | Statische Fixtures | Parametrisierte Factories | Empfehlung |
|---|---|---|---|
| Lesbarkeit | Sehr hoch, Wert direkt sichtbar | Niedriger, Defaults verborgen | Fixtures für einfache Setups |
| Setup-Geschwindigkeit | Sehr schnell, kein Request | Langsamer, API-Call pro Objekt | Fixtures für große Batches |
| Randfall-Abdeckung | Eine Datei pro Fall nötig | Beliebig viele Varianten per Override | Factories für Randfälle |
| Parallel-Sicherheit | Kollisionsrisiko bei geteilten IDs | Eindeutige IDs pro Aufruf | Factories für parallele Worker |
| Wartungsaufwand | Manuell bei Schema-Änderung | Zentral in einer Funktion | Factories bei häufigen Änderungen |
Die Tabelle zeigt, warum eine dogmatische Festlegung auf einen einzigen Ansatz selten die beste Lösung ist: Fixtures gewinnen bei Lesbarkeit und Geschwindigkeit, Factories bei Randfall-Abdeckung und Parallel-Sicherheit. Wer diese Stärken pro Testdatentyp bewusst zuordnet, statt einen Ansatz aus Gewohnheit für alles zu verwenden, reduziert sowohl die Anzahl der Fixture-Dateien als auch die Komplexität der Factories gleichzeitig.
Mironsoft
E2E-Testautomatisierung, Testdaten-Architektur und CI/CD-Integration
Fixtures und Factories richtig einsetzen?
Wir analysieren eure bestehende Testdaten-Architektur, identifizieren überladene Factories und veraltete Fixtures, und bauen ein schlankes, hybrides Modell mit klaren Defaults und gezielten Overrides für Cypress und Playwright.
Factory-Review
Analyse bestehender Factories auf Overengineering und ungenutzte Overrides
Fixture-Bereinigung
Konsolidierung doppelter Fixture-Dateien, Schema-Drift-Absicherung
Hybrid-Architektur
Klare Trennung nach dem 80/20-Prinzip für Cypress- und Playwright-Suiten
10. Zusammenfassung
Die Entscheidung zwischen Fixtures und Factories ist keine Stilfrage, sondern eine Architekturentscheidung mit direkter Wirkung auf Lesbarkeit, Geschwindigkeit und Parallelisierbarkeit der Testsuite. Statische Fixtures liefern maximale Nachvollziehbarkeit und Geschwindigkeit für stabile, selten wechselnde Daten, driften aber, wenn sie das einzige Werkzeug bleiben, in eine unübersichtliche Sammlung fast identischer Dateien. Parametrisierte Factories decken Variation und Randfälle elegant ab und sichern parallele Testläufe strukturell gegen Kollisionen ab, verlieren aber an Lesbarkeit, sobald ihre Defaults und Overrides nicht mehr auf einen Blick nachvollziehbar sind.
Der entscheidende Hebel liegt darin, Overengineering bei Factories konsequent zu vermeiden: eine flache Funktion mit klar benannten Overrides schlägt fast immer eine tief verschachtelte Builder-Hierarchie. Das hybride Modell, stabile Referenzdaten als Fixture und variable Kernentitäten als Factory, deckt in der Praxis die meisten Anforderungen ab, ohne dass ein Team sich für einen einzigen Ansatz entscheiden muss. Wer diese Trennung bewusst nach Zweck trifft, gewinnt eine Suite, die sowohl lesbar als auch flexibel bleibt.
Fixtures vs. Factories: Testdaten richtig erzeugen - Das Wichtigste auf einen Blick
Fixtures vs. Factories
Fixtures für stabile, seltene Referenzdaten. Factories für variable Kernentitäten mit gezielten Overrides.
Lesbarkeit sichern
Overrides im Test explizit benennen, Defaults nicht verstecken, keine tief verschachtelten Factory-Aufrufe.
Overengineering vermeiden
YAGNI für Testdaten: Builder-Chains und Trait-Systeme nur bei echtem, wiederkehrendem Bedarf einführen.
Hybrides Modell
Rund 80 Prozent stabile Fixtures, 20 Prozent variable Factories, kombiniert im selben Test.