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
Section titled “Test stack”| Tool | Purpose |
|---|---|
| Vitest | Test runner and assertions |
| Fishery | Type-safe test data factories |
| MSW | HTTP request interception |
| CommercetoolsMock | In-memory commercetools API |
vitest.config.ts
Section titled “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
Section titled “vitest.setup.ts”The setup file initializes the MSW server with CommercetoolsMock handlers, stubs environment variables, and resets state between tests:
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.
MSW and CommercetoolsMock
Section titled “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
Section titled “Factories”The framework ships a set of Fishery factories for commercetools resources, importable from @evolve-framework/commercetools/factories.
Available factories
Section titled “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()
Section titled “.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 callconst 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
Section titled “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
Section titled “Context helpers”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.
testContextValue()
Section titled “testContextValue()”Creates a full ContextValue with data loaders, store context, client
context, and federated token:
const context = await testContextValue();// context.storeContext → { storeKey: "test", locale: "en-GB", currency: "EUR" }// context.federatedToken → anonymous tokenPass a customer to get an authenticated context:
const customer = await customerDraftFactory.create();const context = await testContextValue(customer);mockContextValue()
Section titled “mockContextValue()”Similar to testContextValue() but accepts a business unit for B2B
scenarios:
const context = await mockContextValue({ customer, businessUnit: await businessUnitDraftFactory.create(),});mockGraphQLContext()
Section titled “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()
Section titled “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
Section titled “Auth helpers”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}"Worked examples
Section titled “Worked examples”DataLoader test
Section titled “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
Section titled “GraphQL resolver test”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); });});Fastify integration test
Section titled “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); });});- 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

