Skip to content

Testing

Evolve gives you tooling for three layers of tests:

Layer What it tests Tooling
Service tests Your modules, API extensions, and payment adapters, against an in-memory commercetools Vitest, MSW, @evolve-framework/commercetools/testing and /factories
Storefront unit and component tests Your site’s helpers, routes, and components Vitest, jsdom, Testing Library, MSW
Test data in a shared environment Back-office steps and cleanup around end-to-end runs The test-support service, evolve simulate, evolve cleanup
Storefront end-to-end Journeys through the deployed site Playwright, @evolve-storefront/testing, evolve add e2e

The commercetools helpers run your code against @labdigital/commercetools-mock, an in-memory commercetools project. MSW intercepts the SDK’s requests and answers them from the mock, so your tests use the real SDK and mock no requests by hand. Use the mock even when your code goes through a dataloader.

Install the test dependencies in your service. Use the @labdigital/commercetools-mock version that @evolve-framework/commercetools names as a peer dependency.

pnpm add -D vitest msw @labdigital/commercetools-mock

Add a setup file and point setupFiles in your Vitest config at it:

// vitest.setup.ts
import { CommercetoolsConfig } from "@evolve-framework/commercetools";
import { rewindFactorySequences } from "@evolve-framework/commercetools/factories";
import {
configureTestClientFactory,
ctMock,
provisionCustomTypes,
provisionPaymentStates,
} from "@evolve-framework/commercetools/testing";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest";
const server = setupServer(...ctMock.getHandlers());
vi.stubEnv("CTP_PROJECT_KEY", "testing");
vi.stubEnv("CTP_API_URL", "http://localhost");
vi.stubEnv("CTP_AUTH_URL", "http://localhost");
vi.stubEnv("CTP_CLIENT_ID", "test");
vi.stubEnv("CTP_CLIENT_SECRET", "test");
vi.stubEnv("CTP_SCOPES", "manage_project:testing");
const provision = async (): Promise<void> => {
await provisionPaymentStates();
await provisionCustomTypes();
};
beforeAll(async () => {
server.listen({ onUnhandledRequest: "error" });
await configureTestClientFactory(await CommercetoolsConfig.load());
await provision();
});
beforeEach(provision);
afterEach(async () => {
server.resetHandlers();
await ctMock.clear();
rewindFactorySequences();
});
afterAll(() => server.close());
  • The mock serves project testing on http://localhost, so the CTP_* variables point there. It accepts any client id and secret.
  • onUnhandledRequest: "error" fails a test that reaches a real API.
  • The mock is strict and is cleared after every test, so the setup provisions the payment states and custom types again before each one, as a deployment provisions them once. The factories create resources that carry these custom types.

Each payment provider package exports the same MSW server as mswServer from its ./testing entry point, for tests that only need the commercetools handlers.

The factories build commercetools drafts with defaults. build() returns the draft, and create() writes it to the mock and returns the resource. Pass only the fields your test cares about.

import {
cartDraftFactory,
customerDraftFactory,
} from "@evolve-framework/commercetools/factories";
import { clientTestFactory } from "@evolve-framework/commercetools/testing";
import { expect, it } from "vitest";
import { cartOwner } from "./cart-owner.ts";
it("finds the customer a cart belongs to", async () => {
const customer = await customerDraftFactory.create();
const cart = await cartDraftFactory.create({ customerId: customer.id });
const owner = await cartOwner(clientTestFactory, cart.id);
expect(owner?.email).toBe(customer.email);
});

clientTestFactory is a ClientFactory that talks to the mock. Pass it wherever your code takes a ClientFactory.

To test HTTP routes, such as a payment provider’s webhook, build the application with createApplication and call app.fastify.inject(...). createApplication returns an initialized application, so you do not need to start it or bind a port.

All from @evolve-framework/commercetools/testing (reference):

Helper Use it for
ctMock The mock itself: getHandlers(), clear(), and project().unsafeAdd(...) to insert a resource as is
ctMockClient() A system request builder against the mock
createAnonymousToken(), createCustomerToken(customer, businessUnit?) Tokens issued by the mock, for code that reads /me
mockGraphQLContext(options) A context value for calling a resolver directly
createTestExtensionContext(options) The context an API extension is called with
TEST_CUSTOM_TYPES The custom type keys provisionCustomTypes provisions

For a GraphQL schema of your own, createGraphQLTestInstance from @evolve-framework/core/testing builds a Yoga instance to send operations to.

The reference sites test their storefront code with Vitest, in a test file next to the code. A *.test.ts file runs in Node, for helpers, middleware, and data mapping. A *.test.tsx file renders a component in jsdom with Testing Library.

Install the test dependencies in your site:

pnpm add -D vitest @vitejs/plugin-react jsdom @testing-library/react msw

frontend/site/vitest.config.ts declares one project per environment. Shortened:

import path from "node:path";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react()],
resolve: { tsconfigPaths: true },
test: {
setupFiles: [path.join(import.meta.dirname, "vitest.setup.ts")],
server: {
// https://github.com/vercel/next.js/issues/77200
deps: { inline: ["next-intl"] },
},
// Declaring any project replaces the top-level include, so declare both.
projects: [
{
extends: true,
test: { name: "site:jsdom", environment: "jsdom", include: ["src/**/*.test.tsx"] },
},
{
extends: true,
test: { name: "site:node", environment: "node", include: ["src/**/*.test.ts"] },
},
],
},
});

The setup file sets the environment variables your code reads, and starts an MSW server without handlers:

import { afterAll, afterEach, beforeAll, vi } from "vitest";
import { mswServer } from "./src/lib/test/mocks.ts";
vi.stubEnv("SERVER_API_GATEWAY_URL", "http://localhost:4000");
vi.stubEnv("CLIENT_API_GATEWAY_URL", "http://localhost:4000");
beforeAll(() => mswServer.listen({ onUnhandledRequest: "error" }));
afterEach(() => mswServer.resetHandlers());
afterAll(() => mswServer.close());

mswServer is setupServer() from msw/node. A test that calls the gateway adds the answer it needs with mswServer.use(...). Any other request fails the test.

Render the component and assert on what the customer sees:

import { render } from "@testing-library/react";
import { expect, it } from "vitest";
import { PriceDisplay } from "./price-display.tsx";
it("shows the currency sign with the amount", () => {
const { container } = render(
<PriceDisplay money={{ centAmount: 205458, currency: "EUR" }} locale="nl-NL" />,
);
expect(container.textContent?.replace(/\s/g, " ")).toBe("€ 2.054,58");
});

A component that uses Next.js or next-intl hooks needs those modules mocked. The reference sites’ search-bar.test.tsx mocks next/navigation and next-intl with vi.mock, and returns the translation key as the text.

Run pnpm test in frontend/site, or pnpm test:ci for coverage. The sites also run their brand’s contrast check as a unit test; see Check contrast.

The @evolve-storefront/* packages test their own components differently: each story runs as a test in headless Chromium, through Storybook’s Vitest addon and @vitest/browser-playwright. That setup lives in the storefront repository’s Storybook project, and a site does not need it.

End-to-end runs need things only a back office does, such as confirming an order or raising an invoice. They also leave data behind that the storefront API cannot delete, such as orders and payments. Two tools do this against a real commercetools project:

  • The test-support service, which end-to-end suites call over HTTP.
  • The evolve simulate and evolve cleanup commands, for the same actions from a terminal or a scheduled job.

Both refuse to run unless the environment opts in, and both delete only test data.

Variable Meaning
SIMULATION_ENABLED Set to true in every environment that may be written to as a test environment. Any other value, including a typo, counts as off. Never set it in production.
TEST_IDENTITY_DOMAIN The reserved domain every test customer’s email is at, such as example.test.
TEST_IDENTITY_PREFIX What every test customer’s email starts with, such as evolve-e2e.

A test identity is an email address at that domain whose local part starts with that prefix. Cleanup deletes only data tied to a test identity, plus anonymous carts, and refuses everything else. Customers created by @evolve-storefront/testing get addresses like evolve-e2e+…@example.test, so example.test and evolve-e2e match them.

The service is its own application. Deploy it to every environment except production.

import { commercetoolsTestSupport } from "@evolve-framework/commercetools/test-support";
import {
CompositeModule,
createApplication,
HealthModule,
} from "@evolve-framework/core";
import {
createTestSupportModule,
testIdentityFromEnvironment,
} from "@evolve-framework/core/test-support";
const app = await createApplication({
name: "test-support",
module: new CompositeModule([
createTestSupportModule({
...commercetoolsTestSupport({
paymentTypeKey: process.env.CTP_PAYMENT_TYPE_KEY!,
}),
secret: process.env.TEST_SUPPORT_SECRET,
testIdentity: testIdentityFromEnvironment(),
}),
new HealthModule({ name: "test-support", graphql: false }),
]),
http: {
address: {
host: process.env.HTTP_HOST ?? "localhost",
port: Number(process.env.HTTP_PORT ?? 4030),
},
},
});
await app.start();
  • It reads the commercetools project from the same CTP_* variables as your other services (CommercetoolsConfig).
  • paymentTypeKey is your payment custom type, which every invoice it raises carries.
  • It refuses to start without SIMULATION_ENABLED=true and a valid test identity.
  • Every request must send authorization: Bearer <secret>. Without a secret, it refuses every request.
  • It has no GraphQL schema, so it cannot join the supergraph.

Each action is a POST to /<surface>/<action>, such as /simulate/order/deliver or /cleanup/order/delete. The requests and responses are defined in @evolve-framework/core/test-support/contract. A refused action, such as deleting a customer that is not a test identity, answers 422 with the reason.

To change how one simulation writes its data, pass that action in overrides. See createTestSupportModule.

Simulate and clean up from the command line

Section titled “Simulate and clean up from the command line”

@evolve-framework/commercetools adds two commands to the Evolve CLI. They write straight to the commercetools project your CTP_* variables name, and print which project that is.

SIMULATION_ENABLED=true pnpm evolve simulate order deliver --order <order-number>
SIMULATION_ENABLED=true pnpm evolve cleanup sweep
  • evolve simulate does what an ERP, a warehouse, or a merchant would: raise, pay, and credit invoices; confirm, ship, and deliver orders; send, withdraw, decline, and renegotiate quotes.
  • evolve cleanup deletes what test identities left behind. Each command takes --test-domain and --test-prefix, and falls back to TEST_IDENTITY_DOMAIN and TEST_IDENTITY_PREFIX.
  • evolve cleanup sweep finds all leftover test data older than 60 minutes, so a run still in flight keeps its data. It changes nothing until you pass --delete. Set the age with --older-than <minutes>.

@evolve-storefront/testing holds the parts a Playwright suite is built from: ops that create data over the API and roll it back, region locators, and matchers. The suite itself lives in your site, under testing/.

Install it, and let evolve add e2e write the wiring:

pnpm add -D @evolve-storefront/testing @playwright/test
pnpm exec evolve --package @evolve-storefront/commands add e2e
pnpm exec playwright install chromium

add e2e writes the Playwright config, an environment table in testing/environments.ts, the fixtures, and one journey spec. It adds the test:e2e and test:e2e:smoke scripts, and never overwrites a file or script you already have unless you pass --force. Fill in every REPLACE-ME in the environment table before the first run.

  • api.run(op) creates data and registers its rollback. Rollbacks run last in, first out, after the test, and a failed rollback fails the test.
  • Orders, payments, quote requests, and carts roll back through the test-support service, because the storefront API cannot delete them. Configure it per environment as testSupport in testing/environments.ts.
  • A spec tagged @backoffice uses simulate ops and is skipped where the environment has no test-support service, such as production.
  • Service tests need no network or credentials: run Vitest in each service.
  • Storefront unit and component tests need no network either: run pnpm test in the site.
  • End-to-end tests run pnpm test:e2e against a deployed environment. E2E_ENV picks the entry in testing/environments.ts, and defaults to test. Under CI, the generated config forbids test.only and retries a failed test twice.
  • Secrets. A spec that uses the api fixture needs TRUSTED_DOCUMENT_BYPASS_SECRET for the target environment, because ops send operations the gateway has not registered. An environment with a testSupport entry also needs TEST_SUPPORT_SECRET.
  • The sweep. Schedule evolve cleanup sweep --delete per test environment, to remove what killed or failed runs left behind. It exits non-zero when a removal fails, so a broken sweep gets noticed.