Using shared fixtures to cement integration testing
April 16, 2026 · Jerome Gill
Integration testing
Integration testing should be the bedrock of testing strategy. They are far easier to write and maintain than end to end tests, and far less fragile than unit.
They are essentially testing the points of interaction of your application, without testing the implementation.
For a standard API, an intergration test makes a HTTP call and checks the response is what you expect.
You might mock service dependencies, or even the DB itself, but the general idea is that if you have an integration test for an API call you can rest assured that it basically works.
This means you can safely mock that call in other parts of the stack. So a client integration test can use a mocked response for a given API route, as long as that route is tested elsewhere.
The Problem
The issue you have is basically that the details of the integration matter. You can’t just send any body to a integration tested route and expect it to work.
We need some guarentee that if I send this request I will get this response.
A backend integration test that mocks out the real response, or a frontend test that hardcodes an expected payload, introduces a fixture that can go stale. The backend changes its response shape. The backend test passes (because it was updated). The frontend test passes (because it tests against a now-stale hardcoded mock).
The root cause in both cases is the same: the fixture that defines “what the API returns” exists in multiple places, and those places are not guaranteed to agree with each other.
The Idea
Fixtures should be a package.
Specifically: a single package in your monorepo that exports a typed object for every API route. Each object has two fields:
req— the canonical example of what a valid request looks like (happy path)res— the subset of the response that every caller can rely on
// packages/fixtures/src/auth/sign-up.fixture.ts
export const signUpFixture = {
req: {
email: 'hello@example.com',
password: '1234!',
name: 'Sign Up User',
},
res: {
user: {
email: 'hello@example.com',
name: 'Sign Up User',
emailVerified: false,
},
},
};
The API integration test uses this fixture to verify the API produces the right output:
const res = await request(app.getHttpServer())
.post('/api/auth/sign-up/email')
.send(signUpFixture.req)
.expect(200);
expect(res.body).toMatchObject(signUpFixture.res);
The client test uses the same fixture to verify the client handles the output correctly — or uses signUpFixture.req as the input for a form test, knowing it represents valid data.
Neither side defines the shape independently. Both sides import from the same place. If the API changes emailVerified to email_verified, the fixture is updated, the API test fails, and the client test fails — at the same time, before anything reaches production.
What the Package Contains
Each route gets a directory with relevant fixture files.
packages/fixtures/src/
auth/
sign-up.fixture.ts ← { req, res }
health/
check.fixture.ts
The fixture file is pure data — no framework dependencies, no special types. It is importable by both the Node.js backend (in integration tests) and the browser frontend (in component tests or mocks).
The fixture covers only the happy path. Error scenarios are tested by mutating the fixture req inline in the test itself:
// The fixture gives us a valid baseline. For an error scenario,
// change only the field that makes it invalid.
await request(server)
.post('/api/auth/sign-in/email')
.send({ ...signInFixture.req, password: 'WrongPassword123!' })
.expect(401);
This keeps the fixture focused and avoids a proliferation of named variants like signUpFixtureWithMissingName or signInFixtureInvalidPassword. The fixture describes what a real interaction looks like. Tests describe what happens when things go wrong.
The Full Integration Spec
With the fixture and manager in place, an integration spec reads at a high level of abstraction. The data is elsewhere; the test expresses intent:
describe('Auth (integration)', () => {
describe('POST /api/auth/sign-up/email', () => {
it('registers a new user', async () => {
const res = await request(server)
.post('/api/auth/sign-up/email')
.send(signUpFixture.req)
.expect(200);
expect(res.body).toMatchObject(signUpFixture.res);
});
it('rejects a duplicate email with 422', async () => {
await request(server).post('/api/auth/sign-up/email').send(signUpFixture.req);
await request(server)
.post('/api/auth/sign-up/email')
.send(signUpFixture.req)
.expect(422);
});
});
describe('POST /api/auth/sign-in/email', () => {
beforeEach(async () => {
await request(server).post('/api/auth/sign-up/email').send(signUpFixture.req);
});
it('returns a session token for valid credentials', async () => {
const res = await request(server)
.post('/api/auth/sign-in/email')
.send(signInFixture.req)
.expect(200);
expect(res.body).toMatchObject(signInFixture.res);
expect(typeof res.body.token).toBe('string');
});
it('rejects wrong password with 401', async () => {
await request(server)
.post('/api/auth/sign-in/email')
.send({ ...signInFixture.req, password: 'WrongPassword123!' })
.expect(401);
});
});
});
Note what’s absent: no inline JSON objects, no hardcoded email strings, no guessing at what the API returns. The fixture is the contract; the test verifies it.
The Boundary Guarantee
The goal of this pattern is not just organisational. The goal is to make the integration boundary between client and API impossible to silently violate.
Consider what happens without shared fixtures:
- Backend developer changes the sign-up response to include
{ user: { verified: false } }instead of{ user: { emailVerified: false } }. - Backend integration test is updated to match.
- Frontend developer has a separate mock that returns
{ user: { emailVerified: false } }. - Frontend tests pass.
- Production: the client checks
response.user.emailVerified, getsundefined.
With shared fixtures:
- Backend developer changes the response.
- They update
signUpFixture.resinfixtures. - The backend integration test still passes — it asserts against the updated fixture.
- Every frontend test that imports
signUpFixturenow fails because it was relying onemailVerified. - The developer is forced to confront the frontend impact before pushing.
The fixture update is the forcing function. It happens in one place and propagates everywhere. The failing frontend tests will need to be dealt with before you have a passing build. A bit annoying, maybe, but much less fragile.