Q11 of 37 · API testing
How do you ensure API tests don't depend on each other (test isolation)?
Short answer
Short answer: Each test creates its own data via API setup, asserts on it, and cleans up. No shared fixtures, no test ordering. If two tests must share state (e.g. expensive setup), make the dependency explicit and document it. Run tests in random order in CI to catch hidden coupling.
Detail
Test isolation is the single biggest factor in suite stability. A coupled suite passes when tests run in the order the author saw, then fails at random when order changes — and CI orders rarely match local.
The principles:
1. Setup in the test, not at the suite level. Each test should call API setup endpoints to create the user, tenant, or records it needs. Suite-level setup that pre-creates "the test users" leaks state across tests:
test('user can update profile', async ({ request }) => {
const user = await createTestUser(request); // ✅ per-test
const res = await request.patch(`/users/${user.id}`, {
data: { name: 'New Name' },
});
expect(res.status()).toBe(200);
});
2. Cleanup, but don't depend on it. Use afterEach to delete created data. But also use unique identifiers (UUID, timestamp suffix) so a missed cleanup doesn't break the next run.
3. No test ordering. If test B works only because test A ran first, that's a bug. Run the suite in a random order at least weekly:
pytest --random-order
mocha --sort=false # or any randomiser
4. Isolate by tenant or namespace where possible. Multi-tenant APIs make this cheap: each test creates a new tenant, owns everything in it, deletes the tenant in cleanup. No data races with parallel runs.
5. Mind shared external state. Even with isolated test data, tests can collide on:
- Rate limits — burning the same API key.
- Email / SMS providers — confirmation email queues.
- Third-party sandboxes — Stripe test data, OAuth provider sessions. Stub or fan out where possible.
6. Document the rare exceptions. Some setup is too expensive to repeat (full database seed, complex multi-step OAuth). Make the dependency explicit:
test.describe.serial('OAuth flow tests', () => {
let consentCode: string;
test('completes consent', async () => { /* sets consentCode */ });
test('exchanges code for token', async () => { /* uses consentCode */ });
});
Now the order is intended, not accidental.
Detection: run the suite in random order in CI. The first time it fails differently from local, you've found a coupling.
The interview signal: per-test setup as default, random order as the canary, and the discipline of making intentional coupling explicit (describe.serial or equivalent).
// EXAMPLE
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ request }) => {
// Create per-test data; never reused across tests
const res = await request.post('/test-fixtures/users', {
data: { suffix: Date.now() },
});
test.info().annotations.push({ type: 'user-id', description: (await res.json()).id });
});
test.afterEach(async ({ request }, info) => {
const userId = info.annotations.find((a) => a.type === 'user-id')?.description;
if (userId) await request.delete(`/test-fixtures/users/${userId}`);
});