Q14 of 37 · API testing
How do you handle test data setup and teardown for an API test suite?
Short answer
Short answer: Set up via the API itself when possible — test fixtures stand on the same contract as production. Use builders to generate unique data per test. Tear down in afterEach, but design tests so they survive a missed cleanup. For expensive seed data, beforeAll with a clear isolation boundary.
Detail
The hierarchy of test-data sources, best to worst:
1. Set up via the API. The cleanest pattern: every test creates the records it needs by calling the same endpoints production clients call. Pros: no separate fixture infrastructure, the tests verify the create endpoints implicitly, easy to evolve.
const user = await createUser(request, { tenantId, role: 'viewer' });
const project = await createProject(request, user.id, 'Demo');
2. Set up via a dedicated test-fixtures endpoint (/test/fixtures/seed). Useful when production endpoints don't expose all the state you need (timestamps in the past, broken records). Mark these endpoints internal-only and never expose them in prod.
3. Set up via direct database insertion. Last resort. Couples tests to schema, breaks on migrations, hides bugs in the create endpoints. Sometimes unavoidable for legacy systems.
4. Pre-seeded shared fixtures. The "test user 1, test user 2" pattern. Fragile — see the test isolation question — but sometimes pragmatic for read-only smoke suites.
Builders / factories make per-test data tractable:
const userFactory = (overrides = {}) => ({
email: `user-${randomUUID()}@test.local`,
password: 'TestPass123!',
role: 'viewer',
...overrides,
});
test('admin can list users', async ({ request }) => {
const admin = await createUser(request, userFactory({ role: 'admin' }));
// ...
});
The unique suffix means parallel tests don't collide on email uniqueness.
Teardown principles:
- afterEach: delete what the test created. Use captured ids, not "everything matching pattern X" (which wipes parallel tests' data).
- Don't depend on cleanup. Use unique identifiers so missed cleanup doesn't break the next run. Daily background reaper for orphaned test data.
- Skip teardown for read-only setup. If a test fixture is genuinely read-only and shared (a static "demo organisation"), creating + deleting per test is wasteful.
Expensive setup pattern: if you genuinely need an expensive setup (full org with 50 users, complex permission graph), use beforeAll for the category of test that needs it, with a clear boundary:
test.describe('admin reporting', () => {
let org: Org;
test.beforeAll(async ({ request }) => {
org = await seedOrgWithUsers(request, 50);
});
test.afterAll(async ({ request }) => {
await deleteOrg(request, org.id);
});
test('admin sees user list', async ({ request }) => { /* ... */ });
});
Anti-patterns:
- A single shared "test user" mutated by every test.
- Cleanup that depends on test order (cleanup in test 5 deletes data test 6 needs).
- Hardcoded ids that collide with other tests in parallel.