Vercel Logo

Testing Strategies

Routing misconfigurations are the most common microfrontends problem. A wrong path pattern can send users to 404 pages. Tests catch these before deployment.

Outcome

Write routing validation tests using @vercel/microfrontends/next/testing utilities.

The Problem: Configuration Drift

As your application grows, routing changes:

  • New paths added to apps
  • proxy.ts matchers updated
  • Redirects added or removed

Without tests, these changes can break routing silently. A redirect in marketing that conflicts with a docs path? You won't know until users complain.

Testing Utilities

The @vercel/microfrontends package includes three test utilities:

UtilityPurpose
validateRoutingVerify paths route to correct apps
validateMiddlewareConfigEnsure proxy.ts matchers work with routing
validateMiddlewareOnFlaggedPathsTest feature flag routing behavior

Fast Track

  1. Install Jest in the marketing app
  2. Write routing validation tests
  3. Add a proxy.ts and test its matcher
  4. Run tests in CI

Hands-on Exercise 3.2

Add routing tests to the Acme Platform.

Part 1: Set Up Jest

Add Jest to the marketing app (where microfrontends.json lives):

cd apps/marketing
pnpm add -D jest @types/jest ts-jest

Create apps/marketing/jest.config.js:

apps/marketing/jest.config.js
/** @type {import('jest').Config} */
const config = {
  testEnvironment: "node",
  transform: {
    "^.+\\.tsx?$": ["ts-jest", { useESM: true }],
  },
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/$1",
  },
  testMatch: ["**/__tests__/**/*.test.ts"],
};
 
module.exports = config;

Add test script to apps/marketing/package.json:

apps/marketing/package.json
{
  "scripts": {
    "dev": "next dev --port 3000",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "test": "jest"
  }
}

Part 2: Write Routing Tests

Create apps/marketing/__tests__/routing.test.ts:

apps/marketing/__tests__/routing.test.ts
/* @jest-environment node */
import { validateRouting } from "@vercel/microfrontends/next/testing";
 
describe("microfrontends routing", () => {
  const configPath = "./microfrontends.json";
 
  test("routes marketing paths to marketing app", () => {
    expect(() =>
      validateRouting(configPath, {
        "acme-marketing": ["/", "/pricing", "/about", "/contact"],
      })
    ).not.toThrow();
  });
 
  test("routes docs paths to docs app", () => {
    expect(() =>
      validateRouting(configPath, {
        "acme-docs": [
          "/docs",
          "/docs/getting-started",
          "/docs/api",
          "/docs/api/reference",
          "/docs/guides/setup",
        ],
      })
    ).not.toThrow();
  });
 
  test("routes dashboard paths to dashboard app", () => {
    expect(() =>
      validateRouting(configPath, {
        "acme-dashboard": [
          "/app",
          "/app/projects",
          "/app/projects/123",
          "/settings",
          "/settings/profile",
          "/settings/billing",
        ],
      })
    ).not.toThrow();
  });
 
  test("404 paths fall through to default app", () => {
    expect(() =>
      validateRouting(configPath, {
        "acme-marketing": ["/unknown-page", "/random/nested/path"],
      })
    ).not.toThrow();
  });
});

The keys are the application keys from microfrontends.json. A typo like "marketing" fails with Application marketing does not exist in the microfrontends config, which is a nice way to find out.

Part 3: Add a Proxy and Test Its Matcher

The marketing app doesn't have a proxy.ts yet. Give it one that runs the microfrontends middleware. With no flags it leaves routing alone for now, and Lesson 4.1 adds a flag to it.

apps/marketing/proxy.ts
import type { NextRequest } from "next/server";
import { runMicrofrontendsMiddleware } from "@vercel/microfrontends/next/middleware";
 
export async function proxy(request: NextRequest) {
  const response = await runMicrofrontendsMiddleware({
    request,
    flagValues: {},
  });
 
  if (response) {
    return response;
  }
}
 
export const config = {
  matcher: ["/.well-known/vercel/microfrontends/client-config"],
};

The package still says "middleware" in its function names. They work in Next.js 16's proxy.ts. The matcher includes /.well-known/vercel/microfrontends/client-config because the browser asks that endpoint which app serves a flagged path.

Create apps/marketing/__tests__/proxy.test.ts:

apps/marketing/__tests__/proxy.test.ts
/* @jest-environment node */
import { validateMiddlewareConfig } from "@vercel/microfrontends/next/testing";
import { config as proxyConfig } from "../proxy";
 
describe("proxy configuration", () => {
  const configPath = "./microfrontends.json";
 
  test("marketing proxy config is compatible with microfrontends", () => {
    expect(() =>
      validateMiddlewareConfig(proxyConfig, configPath)
    ).not.toThrow();
  });
});

This test fails if the matcher catches a path that belongs to a child app, like /docs. Those requests never reach the default app's proxy, so a matcher that claims them is lying to you. It also fails if the client-config path is missing.

Part 4: Run the Tests

cd apps/marketing
pnpm test

Expected output:

PASS  __tests__/routing.test.ts
  microfrontends routing
    ✓ routes marketing paths to marketing app (15 ms)
    ✓ routes docs paths to docs app (3 ms)
    ✓ routes dashboard paths to dashboard app (2 ms)
    ✓ 404 paths fall through to default app (2 ms)

PASS  __tests__/proxy.test.ts
  proxy configuration
    ✓ marketing proxy config is compatible with microfrontends (8 ms)

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total

Catching Misconfigurations

What happens when routing is wrong?

Add a failing test to see the error:

test("EXAMPLE: this would catch a misconfiguration", () => {
  expect(() =>
    validateRouting("./microfrontends.json", {
      // Wrong! /docs should route to acme-docs, not acme-marketing
      "acme-marketing": ["/docs"],
    })
  ).toThrow();
});

Flip .toThrow() to .not.toThrow() and Jest prints exactly what's wrong:

Incorrect microfrontends routing detected:

- Expected /docs to match acme-marketing, but it matched acme-docs (on /docs, /docs/:path*).

Adding Tests to CI

Update turbo.json to include tests:

turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    }
  }
}

Add root-level test script to package.json:

package.json
{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "test": "turbo test",
    "lint": "turbo lint"
  }
}

Now pnpm test runs tests across all apps.

Commit

git add -A
git commit -m "test: add microfrontends routing validation tests"

Done-When

  • Jest configured in marketing app
  • Routing tests pass for all three apps
  • proxy.ts configuration test passes
  • Tests can run via pnpm test from root

Test Coverage Strategy

What to TestWhy
All known pathsEnsure routing doesn't break
Nested pathsVerify wildcard patterns work
Edge cases (404s)Confirm fallback behavior
proxy.ts matchersPrevent matcher conflicts
Feature flag pathsValidate conditional routing

What's Next

Debug headers and tracing for observing routing decisions in production.

Was this helpful?

supported.