Prisma Composer

Testing

Your app's code gets every dependency from one call — service.load() — and nothing from arguments or globals. That makes testing a matter of deciding what load() returns; the code under test is never modified. There are two tools, and you pick by how much of the real path you want to exercise:

You want to… Use From
Call a page / action / handler directly, dependencies faked mockService @prisma/composer/testing
Boot the real built entry and drive it over real HTTP bootstrapService @prisma/composer-prisma-cloud/testing

Most tests are the first kind. Reach for the second when the thing you're proving is the wiring itself — that the service boots, reads its config, builds its clients, and answers requests.

Unit tests — mockService

mockService(service, overrides) returns a copy of the service whose load() yields your fakes and whose config() yields param defaults overlaid with any overrides (one flat object — dependency names route to load(), param names to config()). The fakes are type-checked against the service's declared dependencies, so a fake with the wrong shape doesn't compile.

Substituting the mocked service for the real one is your test runner's job — vi.mock in Vitest, mock.module in bun test. A Vitest example, testing a Next.js page:

// page.test.tsx
import { renderToString } from 'react-dom/server';
import { mockService } from '@prisma/composer/testing';
import realService from '../src/service.ts';

vi.mock('../src/service.ts', () => ({
  default: mockService(realService, {
    auth: { verify: async () => ({ ok: true }) },
  }),
}));

import Page from './page.tsx';

it('renders the verified state', async () => {
  expect(renderToString(await Page())).toContain('Signed in: true');
});

No server, no database, no environment — the page just renders against the fake.

Integration tests — bootstrapService

bootstrapService boots the service's real built entry in-process, fed the same way a deployed boot is fed — you just choose the values. Point a dependency at a stand-in you run on a loopback port, then make real HTTP requests. Run these under bun test:

// service.integration.test.ts
import { bootstrapService } from '@prisma/composer-prisma-cloud/testing';
import fakeAuth from '@my-app/auth/fake'; // an in-memory handler, no db
import storefront from '../src/service.ts';

const fake = Bun.serve({ port: 0, fetch: fakeAuth });

const app = await bootstrapService(storefront, {
  service: { port: 4310 },
  inputs: { auth: { url: fake.url.href } },
});

const res = await app.fetch(new Request(app.url));
expect(await res.text()).toContain('Signed in: true');

Four things to know:

Next.js services need a third argument — a boot function — because the built entry lives inside Next's standalone output. Resolve it with standaloneServerPath, and hand Next the port explicitly (its standalone server reads process.env.PORT, not the service's config):

import { pathToFileURL } from 'node:url';
import { standaloneServerPath } from '@prisma/composer/nextjs/control';

await bootstrapService(storefront, config, async () => {
  process.env.PORT = String(PORT);
  await import(pathToFileURL(standaloneServerPath(storefront.build)).href);
});

The complete working version is examples/storefront-auth/modules/storefront/app/page.integration.test.ts.

Writing good fakes

A dependency's type is its contract, so anything of that shape is a valid fake, and the compiler holds it to that. In increasing order of realism:

One habit pays for all of this: ship each service's fake from its own package as a /fake entry point (outside src/, so it can't reach production). Fake and service then share one contract — when the contract changes, both stop compiling at once, and every consumer's tests find out immediately.

The reasoning behind the two-seam design is in docs/design/10-domains/testing.md.