Skip to main content

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

ToolPurpose
VitestTest runner and assertions
FisheryType-safe test data factories
MSWHTTP request interception
CommercetoolsMockIn-memory commercetools API

Setup

vitest.config.ts

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

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 { 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 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);

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() 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