Przejdź do głównej zawartości

Strategie danych testowych i fixtures

Zarządzanie danymi testowymi w testach pisanych przez agentów polega na zastąpieniu współdzielonych, ręcznie edytowanych fixtures trzema generowanymi zasobami: fabrykami budowanymi ze schematu bazy, nazwanymi seedami scenariuszy dla testów integracyjnych i end-to-end oraz syntetycznymi zbiorami danych bez PII. Agent napisze wszystkie trzy, ale zaufać im można dopiero wtedy, gdy w CI dowodzą ich testy kontraktowe, porównanie seedów i bramka PII.

Ta strona jest dla programistów, którzy utrzymują zestaw testów, i dla tech leadów, którzy odpowiadają za jego seedy. Sytuacja jest znajoma: plik seed skopiowany z produkcji kilka lat temu, 40 testów zakładających, że użytkownik o ID 1 to „John Doe”, i agent, który „naprawia” czerwony test, dopisując do seeda potrzebny mu wiersz. Każde nowe ograniczenie unikalności psuje całą partię testów i nikt już nie wie, które wiersze są jeszcze komuś potrzebne.

  • Blok reguł do AGENTS.md lub CLAUDE.md, dzięki któremu każda fabryka jest deterministyczna, wolna od kolizji i od PII.
  • Prompt generujący fabryki ze schematu, z cechami (traits), oraz polecenie, które uruchamia go bez interakcji w Claude Code, Codeksie lub Cursorze.
  • Projekt seeda scenariuszy, w którym nazwane wiersze odpowiadają kryteriom akceptacji, więc testy end-to-end przestają zależeć od tego, „co akurat jest w bazie”.
  • Prompt do syntetycznych danych o kształcie produkcji, przy którym agent nigdy nie widzi prawdziwego wiersza.
  • Cztery kontrole w CI, które dowodzą poprawności warstwy danych bez czytania fabryk: test kontraktowy fabryk, porównanie seedów, bramkę PII i CODEOWNERS na ścieżkach z danymi.

Jakich danych testowych potrzebuje każda warstwa testów?

Dział zatytułowany „Jakich danych testowych potrzebuje każda warstwa testów?”

Źródło danych dla każdej warstwy ustal, zanim napiszesz prompt. Agent, który wybiera sam, bierze jeden wzorzec i stosuje go wszędzie, najczęściej inserty do bazy w testach jednostkowych.

WarstwaŹródło danychIzolacjaWłaściciel
Jednostkowabuild(): zwykłe obiekty z fabryki, bez bazyNiepotrzebna, nic nie jest zapisywaneAutor testu
Integracyjnacreate(): wiersze z fabryki w transakcji lub w schemacie per workerRollback po każdym teście albo schemat per worker dla kodu, który sam commitujeAutor testu, na harnessie z testowania integracyjnego
End-to-endNazwany seed scenariusza ładowany raz na przebieg plus wiersze oznaczone per testŚwieża baza na przebieg lub na zadanie agentaTech lead (CODEOWNERS)
Staging i demaWygenerowany syntetyczny zbiór o rozkładach jak na produkcjiOsobna baza odbudowywana przez jobTech lead lub zespół platformowy

Kolumna „właściciel” przy agentach waży więcej niż przy ludziach. Test, który pada z powodu brakującego wiersza, podsuwa agentowi łatwe wyjście: dopisać wiersz. Dane jednostkowe i integracyjne tworzy sam test, więc taki skrót widać w diffie. We współdzielonych seedach go nie widać i dlatego mają one właściciela.

Ustal reguły danych testowych, zanim agent napisze fabrykę

Dział zatytułowany „Ustal reguły danych testowych, zanim agent napisze fabrykę”

Agenci powielają wzorce ze swoich danych treningowych, a kilka z nich prowadzi do niestabilnych testów. Zapisz reguły tam, gdzie czyta je każda sesja. Poniższy blok możesz wkleić do AGENTS.md (czyta go Codex oraz Claude Code od 2.1.277 w kanale latest, gdy nie ma CLAUDE.md) albo do CLAUDE.md; w Cursorze dodaj ten sam tekst jako regułę projektu:

## Test data rules
- Test data comes from factories in tests/factories/. Never hardcode IDs, and never
depend on rows another test created.
- Seed Faker once per test file from TEST_SEED (default 20260926) and print the seed
when a test fails. Freeze Faker's reference date with setDefaultRefDate.
- Unique columns use a per-worker sequence plus a per-file run ID
(`user-${workerId}-${runId}-${seq}@example.com`), not Faker values. Vitest
reloads factories in every test file, so the counter alone repeats across files.
faker.helpers.unique does not exist in @faker-js/faker 10.
- Emails use example.com, example.net or example.org only. No real names, addresses
or phone numbers copied from anywhere.
- Traits encode business rules: a 'delivered' order has shippedAt < deliveredAt; an
'outOfStock' product has stock = 0. Totals are computed from line items.
- Do not edit tests/factories/, tests/seeds/ or the seed scripts to make a failing
test pass. Report the failure instead.

Dwie z tych reguł biorą się z konkretnych awarii. Losowe wartości w kolumnach unikalnych zaczynają kolidować, gdy zestaw rośnie albo działa na równoległych workerach, a błąd pojawia się wtedy w pojedynczych przebiegach CI, czyli w najtrudniejszej do zdiagnozowania postaci. Dawniej rozwiązaniem był faker.helpers.unique, ale w @faker-js/faker 10.6.0 (sprawdzone 2026-09-26) tej funkcji nie ma, a agenci wciąż ją piszą na podstawie starszych przykładów. Sekwencja zawierająca ID workera i identyfikator przebiegu pliku pozostaje unikalna nawet wtedy, gdy wiersze są commitowane, co ma znaczenie przy schemacie per worker: Vitest wczytuje moduł fabryki, a z nim licznik, od nowa w każdym pliku testowym. Wszystkie pozostałe wartości nadal pochodzą z zaseedowanego Fakera, więc błąd z danym seedem da się odtworzyć.

  1. Zapisz poniższy prompt jako prompts/factories.md i dopasuj listę encji oraz cechy do swojej domeny.
  2. Uruchom go w swoim narzędziu (zakładki niżej). Agent czyta schemat, pisze po jednej fabryce na encję i test kontraktowy dla każdej z nich.
  3. Sam uruchom testy kontraktowe i przeczytaj raport, nie fabryki. Sekcja o weryfikacji niżej wyjaśnia, czego dowodzi każda kontrola.
  4. Dodaj fabryki i testy kontraktowe w jednym pull requeście. Od tej pory proś o nowe testy „z użyciem fabryk z tests/factories/”.

Uruchom go interaktywnie przez claude albo bez interakcji, gdy chcesz za każdym razem tego samego wyniku. --allowedTools z góry zatwierdza tylko to, czego zadanie potrzebuje (flagi sprawdzone w claude --help w wersji 2.1.283, 2026-09-26):

Okno terminala
# terminal, katalog główny repozytorium
claude -p "$(cat prompts/factories.md)" \
--allowedTools "Bash(npx vitest run *)" Edit Write

Wygenerowana fabryka powinna wyglądać mniej więcej tak. Sprawdzasz jej kształt, nie każdą linię:

tests/factories/user.factory.ts
import { faker } from '@faker-js/faker';
import { users, type NewUser } from '../../src/db/schema';
import { randomUUID } from 'node:crypto';
import type { Tx } from '../integration/db';
// Vitest loads this module once per test file, so seq restarts at 0 in every file.
// runId keeps committed rows from two files on the same worker apart.
const workerId = process.env.VITEST_POOL_ID ?? '0';
const runId = randomUUID().slice(0, 8);
let seq = 0;
export const userTraits = {
admin: { role: 'admin' },
suspended: { status: 'suspended', suspendedAt: faker.date.recent({ days: 30 }) },
newToday: { createdAt: faker.defaultRefDate() },
} satisfies Record<string, Partial<NewUser>>;
export function buildUser(overrides: Partial<NewUser> = {}, trait?: keyof typeof userTraits): NewUser {
seq += 1;
return {
email: `user-${workerId}-${runId}-${seq}@example.com`,
name: faker.person.fullName(),
role: 'member',
status: 'active',
createdAt: faker.date.past({ years: 2 }),
...(trait ? userTraits[trait] : {}),
...overrides,
};
}
export async function createUser(tx: Tx, overrides: Partial<NewUser> = {}, trait?: keyof typeof userTraits) {
const [row] = await tx.insert(users).values(buildUser(overrides, trait)).returning();
return row;
}

Zaseeduj Fakera w pliku startowym testów, żeby błąd dało się odtworzyć na tych samych danych:

// tests/setup.ts (wpisany w setupFiles w konfiguracji Vitest)
import { faker } from '@faker-js/faker';
const seed = Number(process.env.TEST_SEED ?? 20260926);
faker.seed(seed);
faker.setDefaultRefDate(new Date('2026-01-01T00:00:00Z'));
console.info(`faker seed ${seed}; replay with TEST_SEED=${seed}`);

W Pythonie ten sam układ odwzorujesz w factory_boy (3.3.3 w PyPI) z Faker (40.40.0). W TypeScripcie możesz zamiast ręcznie pisanych builderów użyć biblioteki fishery (2.4.0 w npm). Wersje sprawdzone 2026-09-26.

Fabryka zgodna z schema.ts może mimo to wyłożyć się na prawdziwej bazie: na ograniczeniu CHECK dodanym w surowej migracji SQL, na triggerze albo na wartości domyślnej kolumny, o której ORM nie wie. Serwer MCP bazy danych pozwala agentowi odpytać information_schema w kontenerze testowym i wypróbować inserty, zanim złoży raport. Strona o testowaniu integracyjnym pokazuje, jak podłączyć DBHub do jednorazowego kontenera testowego we wszystkich trzech narzędziach, a serwery MCP dla baz danych porównują alternatywy i ich przełączniki tylko do odczytu. Nie używaj @modelcontextprotocol/server-postgres: npm oznacza go jako „no longer supported”, a sam serwer trafił do zarchiwizowanych serwerów referencyjnych (sprawdzone 2026-09-26). Do tej pracy nie podłączaj żadnego z nich do produkcji.

Zaprojektuj seedy scenariuszy dla testów integracyjnych i end-to-end

Dział zatytułowany „Zaprojektuj seedy scenariuszy dla testów integracyjnych i end-to-end”

Seed scenariusza to mały, nazwany zbiór danych, w którym każdy wiersz istnieje, bo potrzebuje go jakieś kryterium akceptacji. „Wygasła subskrypcja, która próbuje eksportu” to scenariusz. „Dziesięciu losowych użytkowników” już nie, bo żaden test nie powie, od którego z nich zależy.

Napisz seed jako kod korzystający z fabryk i eksportuj uchwyt do każdego nazwanego wiersza. Testy odwołują się wtedy do scenario.users.expired, nigdy do ID ani adresu e-mail. Wiersze, które test dokłada na seed, oznaczaj nazwą testu, żeby sprzątanie mogło je usunąć bez ruszania danych referencyjnych.

Jeśli seedowanie trwa dłużej niż kilka sekund, zaseeduj raz bazę-szablon i twórz z niej bazę dla każdego przebiegu poleceniem Postgresa CREATE DATABASE run_db TEMPLATE app_template. Strona o efemerycznych środowiskach wpina to w stos tworzony per zadanie.

Znane dane logowania do testów end-to-end też należą do seeda, ale generuj je na każdy przebieg i czytaj z uchwytów seeda, a nie z README. Stałe hasło w rodzaju TestAdmin123! prędzej czy później trafia na zrzuty ekranu, do trace’ów, a w końcu na współdzielony staging.

Wygeneruj syntetyczne dane o kształcie produkcji bez PII

Dział zatytułowany „Wygeneruj syntetyczne dane o kształcie produkcji bez PII”

Staging, dema i testy wydajnościowe potrzebują danych, które zachowują się jak produkcyjne: realistyczny podział na plany, długi ogon wartości zamówień i pojedyncze patologiczne rekordy. Bezpieczny sposób to dać agentowi kształt produkcji, nigdy jej wierszy. Człowiek albo przejrzany job uruchamia zapytania agregujące (liczności per plan, percentyle wartości zamówień, odsetek zwrotów) i przekazuje agentowi te liczby. Agent pisze zaseedowany generator, który odtwarza je na syntetycznych wartościach. Politykę organizacji, na której opiera się ta zasada, opisuje strona prywatność danych i polityki firmowe dla agentów kodujących.

Weryfikacją jest tabela podsumowania: porównujesz agregaty docelowe z wygenerowanymi, a nie wiersze. Żeby odtworzyć konkretny błąd z produkcji, wygeneruj kształt tego jednego rekordu (imię ze znakami spoza ASCII, pole z wartością null, datę graniczną) na podstawie zgłoszenia, zamiast anonimizować prawdziwy rekord klienta. Zanonimizowany rekord wciąż niesie strukturę, po której da się kogoś rozpoznać.

Skąd wiesz, że dane testowe są poprawne, skoro nie czytasz każdej fabryki?

Dział zatytułowany „Skąd wiesz, że dane testowe są poprawne, skoro nie czytasz każdej fabryki?”

Nie przeglądasz 30 wygenerowanych fabryk linia po linii. Przeglądasz cztery kontrole, które zawodzą, gdy warstwa danych jest błędna, a CI uruchamia je w każdym pull requeście.

1. Test kontraktowy fabryk. To test, o który prosił prompt do fabryk. Wstawienie każdej cechy do prawdziwego schematu czyni z bazy walidator typów, nullowalności, kluczy obcych i ograniczeń CHECK. Asercja unikalności zrobi się czerwona, gdy ktoś zamieni sekwencję na losowy e-mail:

// tests/factories/factories.contract.test.ts (fragment)
import { describe, it, expect } from 'vitest';
import { buildUser, createUser, userTraits } from './user.factory';
import { withRollback } from '../integration/db';
describe('user factory', () => {
it('builds 1,000 users with unique example.com emails', () => {
const emails = Array.from({ length: 1000 }, () => buildUser().email);
expect(new Set(emails).size).toBe(1000);
expect(emails.every((e) => /@example\.(com|net|org)$/.test(e))).toBe(true);
});
it.each(Object.keys(userTraits))('inserts trait %s into the real schema', (trait) =>
withRollback(async (tx) => {
const row = await createUser(tx, {}, trait as keyof typeof userTraits);
if (trait === 'suspended') expect(row.suspendedAt).not.toBeNull();
}));
});

Reguły cech obejmujące wiele wartości, takie jak „znaczniki czasu zawsze rosną” czy „wartość zamówienia równa się sumie pozycji”, lepiej sprawdzać jako właściwości na generowanych wejściach; zobacz testy oparte na właściwościach.

2. Porównanie seedów. Zaseeduj bazę dwa razy od zera i porównaj zrzuty. Każda różnica to niedeterminizm, który wróci jako niestabilny test. db:reset i db:seed oznaczają tu skrypty twojego projektu. Od wydań poprawkowych z sierpnia 2025 (17.6, 16.10, 15.14, 14.19, 13.22) pg_dump wpisuje do każdego zrzutu w formacie tekstowym linię \restrict z losowym kluczem, więc dwa zrzuty identycznych danych zawsze się różnią, chyba że podasz stały --restrict-key (starsze wersje pg_dump odrzucą tę flagę; tam ją pomiń):

Okno terminala
# CI lub terminal, wyłącznie na jednorazowej bazie testowej
npm run db:reset && npm run db:seed
pg_dump --data-only --inserts --restrict-key=seedcheck "$TEST_DATABASE_URL" > /tmp/seed-a.sql
npm run db:reset && npm run db:seed
pg_dump --data-only --inserts --restrict-key=seedcheck "$TEST_DATABASE_URL" > /tmp/seed-b.sql
diff /tmp/seed-a.sql /tmp/seed-b.sql && echo "seed is deterministic"

3. Bramka PII. Tania kontrola, która przerywa build, gdy w danych testowych pojawi się adres e-mail spoza domen przykładowych. Łapie najczęstszy wyciek, czyli adres wklejony ze zgłoszenia do supportu. Nie wykrywa imion ani adresów pocztowych, dlatego prompt do danych syntetycznych w ogóle nie dostaje wierszy:

Okno terminala
# krok CI: pada, jeśli znajdzie e-mail spoza example.com/.net/.org
! grep -rEoh '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[a-z]{2,}' tests/ seed-output/ \
| grep -vE '@example\.(com|net|org)$'

4. Własność. Obejmij tests/factories/, tests/seeds/ i skrypty seedujące regułą CODEOWNERS z tech leadem jako właścicielem i trzymaj w AGENTS.md zasadę „nie edytuj, żeby test przeszedł”. Pull request, w którym agent zmienił seed razem z czerwonym testem, to wzorzec do odrzucenia. Ochrona wyroczni opisuje hooki, które blokują takie zmiany już w trakcie sesji.

Z tego podziału wynika, kto zatwierdza. CI zatwierdza test kontraktowy, porównanie seedów i bramkę PII. Tech lead zatwierdza zmiany fabryk i seedów przez CODEOWNERS. Autor testu zatwierdza dane lokalne dla testu. Czy zestaw zbudowany na tych danych faktycznie łapie błędy, ocenisz miarami ze strony o sile wyroczni.

Co się psuje, gdy agenci zarządzają danymi testowymi?

Dział zatytułowany „Co się psuje, gdy agenci zarządzają danymi testowymi?”

Agent wywołuje faker.helpers.unique i zestaw się nie kompiluje. W @faker-js/faker 10 tego helpera już nie ma. Jak naprawić: wskaż agentowi blok reguł, poproś o zastąpienie każdego wywołania sekwencją per worker, a potem uruchom test kontraktowy.

Test pada w pojedynczych przebiegach CI i nigdy lokalnie. Zwykle winne jest niezaseedowane wywołanie Fakera, new Date() w fabryce albo unikalna wartość kolidująca między równoległymi workerami. Jak naprawić: uruchom test ponownie z TEST_SEED wypisanym w logu błędu. Jeśli błąd się powtarza, popraw fabrykę; jeśli nie, szukaj zależności od czasu lub od workera i przenieś ją za setDefaultRefDate albo ID workera.

Agent dopisuje wiersz do współdzielonego seeda, żeby jego test przeszedł. Test przechodzi teraz z powodu niezwiązanego z kodem. Jak naprawić: cofnij zmianę seeda, poproś o utworzenie wiersza w samym teście przez fabrykę i dodaj CODEOWNERS na ścieżki seedów, jeśli ta zmiana przeszła review.

Fabryki rozjeżdżają się ze schematem po migracji. Nowe kolumny NOT NULL psują każde create(). Jak naprawić: pierwszy padnie test kontraktowy i wskaże kolumnę. Uruchom ponownie prompt do fabryk z dopiskiem „update the existing factories for the migration in migrations/<file>; add defaults for new required columns”.

Głębokie łańcuchy kluczy obcych spowalniają testy integracyjne. Każde createOrder() tworzy użytkownika, organizację i plan. Jak naprawić: dane referencyjne (plany, role, kraje) twórz raz na worker w beforeAll, rodziców przekazuj jawnie tam, gdzie test tworzy wiele dzieci, a inserty grupuj przez createXList.

Ktoś wkleja do promptu rekord klienta, żeby odtworzyć błąd. Te dane są teraz w transkrypcie sesji i być może w logach dostawcy. Jak naprawić: zarejestruj incydent i poproś o usunięcie danych tak, jak opisuje strona prywatność danych i polityki firmowe dla agentów kodujących, a potem odtwórz kształt rekordu syntetycznie opisaną wyżej metodą.