Testing micro-frontends: a practical strategy
Each team can test its own remote well and the product can still break where the pieces meet. A testing strategy for micro-frontends puts most of its extra effort on those seams.
What changes compared to a single app
In a single application, one test run checks one build. In a micro-frontend platform, what users see is assembled in the browser from parts that were built and released at different times. No pipeline produces the whole application. The combination running in production today may never have existed in any test environment.
So the strategy has two goals. Each remote must be testable alone, quickly. And the contracts between host and remotes must be verified without forcing everyone to release together.
The layers
| Layer | What it catches | Where it runs | Owner |
|---|---|---|---|
| Unit and component tests | Logic and rendering bugs inside a remote | Remote pipeline | Remote team |
| Remote integration tests | The remote working alone with a mocked host | Remote pipeline | Remote team |
| Contract tests | Breaking changes between host and remotes | Host and remote pipelines | Both sides |
| End-to-end journeys | Broken critical flows in the composed product | Before and after a deploy | Platform team and journey owners |
Unit and component tests per remote
A remote is a normal application. Its unit and component tests run in its own pipeline, with the runner the team prefers, and they need nothing from other teams. Most of the test volume should live at this level.
One rule matters: the remote must also run standalone, with a small mock of what the host provides (user, permissions, navigation, theme). That mock makes the remote's integration tests fast and stable. It is the same piece used for local development.
Contract tests between host and remotes
The contract is everything the two sides assume about each other:
- The names of the exposed modules and what they export.
- The routes each remote owns.
- The API the host gives to remotes: session, user, permissions, navigation.
- The events exchanged between areas and their payloads.
Put the contract in a small, versioned package of types. Then verify it on both sides:
- Remote side. A test loads the built remote and checks that every exposed module exists and exports what the contract says.
- Host side. A test mounts a minimal fake remote that follows the contract and checks that the host loads it, routes to it and passes the session.
- Events. Each published event has a schema. The producer tests that it emits valid payloads. The consumer tests that it handles the examples of the schema.
Pact is a good fit for the HTTP APIs each remote consumes, and its message support can cover event payloads. For the contract between host and remote, a types package and the tests above are usually enough. The event side is described in communication between micro-frontends.
End-to-end tests of critical journeys
End-to-end tests against the composed product are slow and expensive to keep stable, so keep few of them. Choose the journeys where a failure costs money or trust: sign in, the main transaction flow, anything that crosses two or more areas.
- Use one tool for the whole platform. Playwright is a common choice.
- Give every journey an owner. A failing test with no owner ends up disabled.
- Leave the detailed behavior of each screen to the remote's own tests.
If every remote must pass the full end-to-end suite of every other team before it can release, you have rebuilt a coupled release process. Run a short smoke set as a gate and the full suite on a schedule.
Testing with different remote versions
The key question before a release is: does my new version work with what is in production right now?
A practical way to answer it:
- Build the new version of the remote and publish it to a preview URL.
- Run the smoke journeys against the production host and the production versions of all other remotes, with only your remote replaced.
- Replace it by overriding the manifest. Playwright can intercept the manifest request with
page.routeand return a modified copy.
The host does the same in reverse: its new version is tested against the production versions of all remotes.
This checks the real combination that will exist after the deploy. A shared staging environment where every team deploys unreleased work cannot tell you that.
After the deploy
Because the final assembly happens in the browser, the last test runs in production. After each release, run the smoke journeys against the live site, with accounts and data set aside for that purpose, and watch the error rate of the changed remote. If either one fails, roll back through the manifest. See error isolation and resilience and CI/CD for micro-frontends.
More in this section
- Monorepo vs polyrepo for micro-frontends
- From frontend monolith to micro-frontends, step by step
- Running a legacy AngularJS app inside a micro-frontend host
- Team ownership, boundaries and Conway's law
Planning a migration or a micro-frontend platform?
In a 20-minute call you describe your situation and I tell you how I'd approach it. If it isn't worth doing yet, I'll say so.
Message me on LinkedIn