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 |
Service tests
Section titled “Service tests”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-mockSet up the mock
Section titled “Set up the mock”Add a setup file and point setupFiles in your Vitest config at it:
// vitest.setup.tsimport { 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
testingonhttp://localhost, so theCTP_*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.
Write a test
Section titled “Write a test”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.
Other helpers
Section titled “Other helpers”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.
Storefront unit and component tests
Section titled “Storefront unit and component tests”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 mswConfigure Vitest
Section titled “Configure Vitest”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.
Test a component
Section titled “Test a component”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.
Test data in a shared environment
Section titled “Test data in a shared environment”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 simulateandevolve cleanupcommands, 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.
Opt in
Section titled “Opt in”| 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.
Run the test-support service
Section titled “Run the test-support service”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). paymentTypeKeyis your payment custom type, which every invoice it raises carries.- It refuses to start without
SIMULATION_ENABLED=trueand a valid test identity. - Every request must send
authorization: Bearer <secret>. Without asecret, 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 sweepevolve simulatedoes 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 cleanupdeletes what test identities left behind. Each command takes--test-domainand--test-prefix, and falls back toTEST_IDENTITY_DOMAINandTEST_IDENTITY_PREFIX.evolve cleanup sweepfinds 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>.
Storefront end-to-end tests
Section titled “Storefront end-to-end tests”@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/testpnpm exec evolve --package @evolve-storefront/commands add e2epnpm exec playwright install chromiumadd 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
testSupportintesting/environments.ts. - A spec tagged
@backofficeusessimulateops and is skipped where the environment has no test-support service, such as production.
Run tests in CI
Section titled “Run tests in CI”- Service tests need no network or credentials: run Vitest in each service.
- Storefront unit and component tests need no network either: run
pnpm testin the site. - End-to-end tests run
pnpm test:e2eagainst a deployed environment.E2E_ENVpicks the entry intesting/environments.ts, and defaults totest. UnderCI, the generated config forbidstest.onlyand retries a failed test twice. - Secrets. A spec that uses the
apifixture needsTRUSTED_DOCUMENT_BYPASS_SECRETfor the target environment, because ops send operations the gateway has not registered. An environment with atestSupportentry also needsTEST_SUPPORT_SECRET. - The sweep. Schedule
evolve cleanup sweep --deleteper 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.

