# Build test data around intent instead of table shape

> Use semantic builders, explicit variants, isolated ownership, and deliberate invalid-data escapes so schema changes do not obscure what tests mean.

Canonical URL: https://www.devobs.io/articles/test-data-builders-by-intent/
By: Nina Patel
Published: 2026-01-01T20:32:24.309Z
Updated: 2026-09-05
Section: Architecture

Test data should explain the scenario, not repeat the production schema. Give tests small builders with valid semantic defaults, explicit variants, and clear cleanup ownership. Keep low-level escape hatches for tests that intentionally create invalid or historical states.

## Compare the common patterns

Shared static fixtures are fast to start but accumulate unrelated fields and hidden coupling. A change made for one test can silently alter many others. They are best for immutable reference data, such as a fixed currency list, not mutable users and orders.

Factories create valid objects with overrides. They reduce repetition but can become bags of every database column. Object Mothers provide named examples, such as a suspendedMember, but a growing catalog of combinations becomes difficult to compose. Builders work well when they expose domain choices in steps and delay persistence until build or create.

The useful boundary is semantic. Prefer member().inWorkspace(acme).suspended() over userFactory({status: 4, workspace_id: 17}). The first records why the values exist and can absorb a schema change behind the builder.

## Choose minimal valid defaults

Defaults should produce the smallest object accepted by current invariants, with deterministic values. Avoid random data unless the test is explicitly probabilistic. Random emails and times make failures hard to reproduce and snapshots noisy.

Do not hide behaviorally important values. A test about authorization should state role and tenant. A test about expiry should control the clock and expiration. Defaults are for irrelevant plumbing such as a harmless display name.

When a new required database field appears, update the builder once with a neutral semantic default. If no neutral value exists, that is a signal that affected tests should choose explicitly.

Pytest’s [fixture documentation](https://docs.pytest.org/en/stable/how-to/fixtures.html) describes fixture composition, scopes, and yield-based teardown. Use fixtures for resource lifecycle and builders for scenario data. A database fixture can own a transaction while each test builds the users it needs.

## Own isolation and cleanup

Parallel tests cannot share mutable identities, workspace names, queues, or clock state. Give each test or worker a namespace and enforce uniqueness in the builder. Avoid cleanup queries such as delete all test users; they can erase another worker’s data.

Playwright’s [test fixture documentation](https://playwright.dev/docs/test-fixtures) shows fixture setup and teardown and supports worker-scoped resources. Browser contexts should remain test scoped, while an expensive isolated account may be worker scoped if its data namespace is unique.

Prefer rollback, disposable databases, or ownership-tagged deletion. Cleanup must run after failure and should report leftovers. If tests share a service that cannot isolate data, serialize only that group and make the limitation visible.

## Support relationships and variants

Builders should create a graph in dependency order and return handles to important nodes. An invoice scenario may create tenant, buyer, product, price version, invoice, and line items, but the test should name only the nodes it asserts.

Use composable variants such as withExpiredPlan and withExternalCollaborator. Detect incompatible variants rather than letting the last override win silently. Keep persistence explicit so pure unit tests can build values without touching the database.

Invalid-data tests need a clearly named unsafe path that bypasses ordinary validation while preserving control of the exact violation. Do not weaken the normal builder for everyone. A migration test might insertLegacyRowWithoutCurrency while production-like tests continue to create valid money.

## Review the test’s story

A reader should identify given, action, and expected outcome without knowing column names. Track builder usage and remove options that expose storage details without serving a scenario.

Pick the noisiest fixture file and rewrite three tests using a semantic builder. Run them in parallel, add one new required schema field, and verify that only the builder changes unless the field truly matters to a test’s intent.

## Source references

- <https://docs.pytest.org/en/stable/how-to/fixtures.html>
- <https://playwright.dev/docs/test-fixtures>
