Backend testing
Evolve backend services use Vitest as the test runner, Fishery for test data factories, and MSW with CommercetoolsMock for HTTP mocking. This guide covers the setup, patterns, and worked examples.
Test stack
| Tool | Purpose |
|---|---|
| Vitest | Test runner and assertions |
| Fishery | Type-safe test data factories |
| MSW | HTTP request interception |
| CommercetoolsMock | In-memory commercetools API |
Setup
vitest.config.ts
Each service has a Vitest config that sets up path aliases and global test configuration:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["./vitest.setup.ts"],
},
});
vitest.setup.ts
The setup file initializes the MSW server with CommercetoolsMock handlers, stubs environment variables, and resets state between tests:
import { ctMock } from "@evolve-framework/commercetools/testing";
import { mswServer } from "#src/testing/mocks.ts";
import { rewindFactorySequences } from "./factories";
import { afterAll, afterEach, beforeAll } from "vitest";
beforeAll(() => {
mswServer.listen({ onUnhandledRequest: "warn" });
});
afterEach(() => {
mswServer.resetHandlers();
ctMock.clear();
rewindFactorySequences();
});
afterAll(() => {
mswServer.close();
});
The rewindFactorySequences() call resets Fishery's auto-increment
counters so each test starts from sequence: 1.
MSW and CommercetoolsMock
CommercetoolsMock provides an in-memory implementation of the
commercetools API. Its request handlers are loaded into MSW so that
any fetch() call to the commercetools API is intercepted and handled
in-process.
import { CommercetoolsMock } from "@labdigital/commercetools-mock";
import { setupServer } from "msw/node";
export const ctMock = new CommercetoolsMock({
apiHost: "http://localhost",
authHost: "http://localhost",
enableAuthentication: true,
validateCredentials: false,
defaultProjectKey: "testing",
silent: true,
strict: true,
});
export const mswServer = setupServer(...ctMock.getHandlers());
To add custom MSW handlers for non-commercetools endpoints (e.g.
external APIs), use mswServer.use():
import { http, HttpResponse } from "msw";
mswServer.use(
http.post("https://external-api.example.com/webhook", () => {
return HttpResponse.json({ ok: true });
}),
);
Factories
Twenty-five Fishery factories provide realistic test data for every
commercetools resource type. They live in
vendor/packages/commercetools/src/factories/.
Available factories
associateRoleDraftFactory businessUnitDraftFactory
cartDiscountDraftFactory cartDraftFactory categoryDraftFactory
channelDraftFactory customerDraftFactory
discountCodeDraftFactory messageDraftFactory orderDraftFactory
paymentDraftFactory productDiscountDraftFactory
productDraftFactory productTypeDraftFactory
shippingMethodDraftFactory shoppingListDraftFactory
standalonePriceDraftFactory stateDraftFactory
storeDraftFactory taxCategoryDraftFactory zoneDraftFactory
.build() vs .create()
.build()returns a plain object in memory, useful when you only need the data shape without persisting it.create()posts the draft to CommercetoolsMock via theonCreatehook and returns the created resource with a generatedid,version, and timestamps
// In-memory only - no API call
const draft = customerDraftFactory.build({ email: "test@example.com" });
// Persisted to CommercetoolsMock - has id, version, etc.
const customer = await customerDraftFactory.create({ email: "test@example.com" });
Use .createList(n) to create multiple resources at once:
const products = await productDraftFactory.createList(5);
Customizing factories
Override any field by passing attributes:
const customer = await customerDraftFactory.create({
email: "vip@example.com",
firstName: "Jane",
lastName: "Doe",
});
Factories use sequence for auto-incrementing values (e.g.
customer-{sequence}@example.com), ensuring unique values across
tests.
Context helpers
The testing package provides helpers to create realistic GraphQL context objects for resolver tests.
testContextValue()
Creates a full ContextValue with data loaders, store context, client
context, and federated token:
import { testContextValue } from "#src/testing/context-value.ts";
const context = await testContextValue();
// context.storeContext → { storeKey: "test", locale: "en-GB", currency: "EUR" }
// context.federatedToken → anonymous token
Pass a customer to get an authenticated context:
const customer = await customerDraftFactory.create();
const context = await testContextValue(customer);
mockContextValue()
Similar to testContextValue() but accepts a business unit for B2B
scenarios:
const context = await mockContextValue({
customer,
businessUnit: await businessUnitDraftFactory.create(),
});
mockGraphQLContext()
Returns a mock context with custom client factories, useful when you need to control the commercetools client behaviour directly:
const { storeContext, clientContext } = await mockGraphQLContext({
customer,
clientFactory: () => customRequestBuilder,
});
createTestStoreContext()
Creates a standalone store context for tests that don't need the full GraphQL context:
const storeContext = createTestStoreContext({
storeKey: "de-store",
locale: "de-DE",
currency: "EUR",
});
Auth helpers
Two helpers create federated tokens for anonymous and authenticated sessions:
import {
createAnonymousToken,
createCustomerToken,
} from "#src/testing/auth.ts";
const anonToken = createAnonymousToken();
// sub: "anonymous_id:1234"
const customer = await customerDraftFactory.create();
const customerToken = createCustomerToken(customer);
// sub: "customer_id:{customer.id}"
Worked examples
DataLoader test
import { storeDraftFactory } from "@evolve-framework/commercetools/factories";
import { clientTestFactory } from "@evolve-framework/commercetools/testing";
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
describe("storeByKey", () => {
let loader;
let stores;
beforeAll(() => {
const ctApi = clientTestFactory.getSystemRequestBuilder();
loader = createStoreByKeyLoader(ctApi);
});
beforeEach(async () => {
stores = await storeDraftFactory.createList(2);
});
test("loads a store by key", async () => {
const result = await loader.load(stores[0].key);
expect(result?.key).toEqual(stores[0].key);
});
test("returns null for non-existing key", async () => {
const result = await loader.load("non-existing-key");
expect(result).toBeNull();
});
});
GraphQL resolver test
import { testContextValue } from "#src/testing/context-value.ts";
import { customerDraftFactory } from "@evolve-framework/commercetools/factories";
describe("customer resolver", () => {
test("returns the current customer", async () => {
const customer = await customerDraftFactory.create();
const context = await testContextValue(customer);
const result = await resolvers.Query.me({}, {}, context);
expect(result.email).toEqual(customer.email);
});
});
Fastify integration test
For testing HTTP endpoints directly, use Fastify's inject() method:
import { buildApp } from "../app";
describe("POST /api/webhook", () => {
test("processes a valid webhook", async () => {
const app = await buildApp();
const response = await app.inject({
method: "POST",
url: "/api/webhook",
payload: { event: "payment.completed", id: "tx-123" },
});
expect(response.statusCode).toBe(200);
});
});
Tips
- Keep factories stateless. Use
.create()inbeforeEach, not at module level - Use
mswServer.use()for one-off handler overrides in individual tests; they're automatically cleaned up inafterEach - The
strict: trueoption on CommercetoolsMock throws on unexpected API calls, catching accidental requests early - Run tests with
pnpm testfrom the service directory orpnpm testat the root to run all backend tests