Skip to content

New: AI agent integration via Model Context Protocol (MCP).Learn more

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.

Tool Purpose
Vitest Test runner and assertions
Fishery Type-safe test data factories
MSW HTTP request interception
CommercetoolsMock In-memory commercetools API

Each service has a Vitest config that sets up path aliases and global test configuration:

vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["./vitest.setup.ts"],
},
});

The setup file initializes the MSW server with CommercetoolsMock handlers, stubs environment variables, and resets state between tests:

vitest.setup.ts
import { rewindFactorySequences } from "@evolve-framework/commercetools/factories";
import { ctMock } from "@evolve-framework/commercetools/testing";
import { afterAll, afterEach, beforeAll } from "vitest";
import { mswServer } from "#src/testing/mocks.ts";
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.

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 });
}),
);

The framework ships a set of Fishery factories for commercetools resources, importable from @evolve-framework/commercetools/factories.

associateRoleDraftFactory businessUnitDraftFactory cartDiscountDraftFactory cartDraftFactory categoryDraftFactory channelDraftFactory customerDraftFactory discountCodeDraftFactory messageDraftFactory orderDraftFactory paymentDraftFactory productDiscountDraftFactory productDraftFactory productTypeDraftFactory shippingMethodDraftFactory shoppingListDraftFactory standalonePriceDraftFactory stateDraftFactory storeDraftFactory taxCategoryDraftFactory zoneDraftFactory

  • .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 the onCreate hook and returns the created resource with a generated id, 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);

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.

The framework’s testing utilities provide helpers to create realistic GraphQL context objects for resolver tests. mockGraphQLContext, createAnonymousToken, and createCustomerToken are exported from @evolve-framework/commercetools/testing; the context-value helpers (testContextValue, mockContextValue, createTestStoreContext) live in the framework’s src/testing/ directory and are used by the framework’s own resolver tests, where most resolvers now live.

Creates a full ContextValue with data loaders, store context, client context, and federated token:

packages/commercetools/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);

Similar to testContextValue() but accepts a business unit for B2B scenarios:

const context = await mockContextValue({
customer,
businessUnit: await businessUnitDraftFactory.create(),
});

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,
});

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",
});

Two helpers create federated tokens for anonymous and authenticated sessions:

import {
createAnonymousToken,
createCustomerToken,
} from "@evolve-framework/commercetools/testing";
const anonToken = createAnonymousToken();
// sub: "anonymous_id:1234"
const customer = await customerDraftFactory.create();
const customerToken = createCustomerToken(customer);
// sub: "customer_id:{customer.id}"
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();
});
});

Resolver tests live alongside the resolvers in the framework modules, where testContextValue is imported from the local testing utilities:

import { customerDraftFactory } from "@evolve-framework/commercetools/factories";
import { testContextValue } from "#src/testing/context-value.ts";
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);
});
});

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);
});
});
  • Keep factories stateless. Use .create() in beforeEach, not at module level
  • Use mswServer.use() for one-off handler overrides in individual tests; they’re automatically cleaned up in afterEach
  • The strict: true option on CommercetoolsMock throws on unexpected API calls, catching accidental requests early
  • Run tests with pnpm test from the service directory or pnpm test at the root to run all backend tests