Fixtures vs. Factories: Testdaten richtig erzeugen
PASS
expect()
Testing · E2E · Cypress · Playwright
Fixtures vs. Factories: Testdaten richtig erzeugen
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.

16 Min. Lesezeit Fixtures · Factories · Overrides Cypress · Playwright · TypeScript

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.

11. FAQ: Fixtures vs. Factories

1Was ist der Unterschied zwischen einer Fixture und einer Factory?
Eine Fixture liefert bei jedem Lauf exakt dieselben Werte. Eine Factory erzeugt Daten zur Laufzeit, mit Standardwerten und der Möglichkeit, einzelne Felder gezielt zu überschreiben.
2Wann sollte ich eine statische Fixture statt einer Factory verwenden?
Für kleine, stabile Datensätze, exotische Randfälle und Tests, bei denen der exakte, reproduzierbare Wert Teil der Testaussage ist.
3Wann sollte ich eine Factory statt einer Fixture verwenden?
Bei vielen Variationen, die mit endlich vielen Fixture-Dateien unübersichtlich würden, und bei parallelen Testläufen mit kollisionsfreien IDs.
4Warum werden Factories mit der Zeit schwerer lesbar?
Weil ein kurzer Aufruf nicht zeigt, welche übrigen Felder mit welchen Defaults befüllt werden. Der volle Zustand steckt in der Factory-Implementierung.
5Was ist ein Warnsignal für Factory-Overengineering?
Eine Builder-Chain mit vielen kaum genutzten with-Methoden, oder ein Trait-System, das jede denkbare Zukunftskombination vorab abbilden will.
6Wie sorgt eine Factory für Parallel-Sicherheit in E2E-Tests?
Durch einen eindeutigen Bezeichner pro Aufruf, etwa eine zufällige E-Mail oder eine SKU mit Zeitstempel, sodass parallele Worker sich nie überschreiben.
7Kann ich eine Fixture und eine Factory für dasselbe Test-Entity kombinieren?
Ja, häufig die robusteste Lösung: eine Fixture als stabiler Basiswert, eine schlanke Factory für gezielte Varianten auf derselben Basis.
8Was bedeutet das 80 zu 20 Prinzip bei Fixtures und Factories?
Rund 80 Prozent der Testdaten bleiben stabil und eignen sich für Fixtures, die restlichen 20 Prozent brauchen echte Variation und werden über Factories erzeugt.
9Wie vermeide ich, dass Factory-Defaults im Test versteckt bleiben?
Indem der für den Test relevante Zustand als explizites, benanntes Override im Testcode steht, statt sich auf verborgene Defaults zu verlassen.
10Lohnt sich eine Builder-Chain für Testdaten-Factories?
Meist nicht. Eine flache Factory-Funktion mit einfachem Overrides-Objekt deckt dieselben Fälle ab und ist deutlich wartbarer als eine Builder-Hierarchie.