On this page
- What is microservices testing, and what is it for?
- Testing in microservices: why distributed systems are hard to test
- The testing pyramid for microservices
- How to test microservices step by step
- Integration testing for microservices
- End-to-end testing microservices
- Testing synchronous and asynchronous flows in microservices
- Testing durable workflows
- Testing microservices with real databases, queues and workflows
- Performance, chaos and security testing for microservices
- What AI coding agents change about testing microservices
- Do you need a staging environment to test microservices at all?
- Building a microservices testing strategy
- Test automation tools vs environment platforms
- Running the shared test environment like a product
- Microservices testing best practices checklist
- How lightweight ephemeral environments keep every test running against current dependencies
- Where to go next
Microservices testing is the practice of checking that each service in a distributed system works on its own, and that the services keep working when they call each other or hand work off through queues and workflows. It exists because a microservices architecture moves correctness out of any single codebase and into the calls, messages and workflow steps between services that different teams build, deploy, and change on different schedules.
The biggest risk is not a missing test. It is a suite that passes at every level while the thing it ran against no longer matches production: a mock written against last quarter’s API, a staging environment three deploys behind, a dependency that changed its response shape on Tuesday. Coding agents make this worse by volume. CircleCI’s 2026 report found engineering throughput up 59 percent year over year while roughly 30 percent of merge attempts fail, which means more changes reaching the seams between services and fewer of them verified there first. The complete guide to software factories looks at the same gap from the delivery side, as agents scaling output faster than verification.
This guide covers what microservices testing is, why distributed cloud-native systems are hard to test, and the test types they need: unit, contract, integration, end-to-end, performance, chaos and security. It shows how to test microservices at each stage from a laptop to production, how to test synchronous REST and gRPC calls and asynchronous flows through Kafka, SQS, RabbitMQ and Pub/Sub, and how durable workflow engines change the job. It then covers real databases and queues, what coding agents change, whether you still need a staging environment, how to build a strategy, which tools belong to which layer, and a best practices checklist. The complete guide to preview environments covers the per-pull-request environment model and the platforms that offer it, and the complete guide to Kubernetes testing covers Kubernetes as the platform those tests run on.
What is microservices testing, and what is it for?
Microservices testing validates two things: the behavior of an individual service in isolation, and the behavior of the system when services interact. Unit and component tests cover the first. Contract, integration, and end-to-end tests cover the second, and performance, chaos and security tests check how the whole system holds up under load, under failure and under attack. People also call it testing microservices, microservice testing, testing in microservices, testing of microservices, or distributed systems testing, and the practice is the same under each name.
It is for catching three classes of problem before users do: logic bugs inside a service, contract breaks between services, and behavior that only appears when real services run together under real conditions. It is not a substitute for observability in production, and it is not a way to prove a system correct. It narrows the gap between what a test checked and what will run.
In a monolith, most of the second category does not exist. A function calling another function shares a process, a compiler, and a deploy, so a changed signature breaks the build.
In a microservices architecture, the same call crosses a network to a service that may be on a different version, written in a different language, and owned by a team you have never met. Nothing breaks at build time. It breaks at runtime, in whichever environment first runs the two versions together.
That shifts where the effort goes. Testing a monolith is mostly about coverage of logic. Testing microservices is mostly about coverage of interactions: which services call which, with what payloads, through which queues, and under what failure conditions. It is also about whether the version of each service you tested against is the version that will be running when your change ships.
Kubernetes made this the default problem rather than a niche one. The CNCF’s 2025 annual survey, published in January 2026, found that 82 percent of container users now run Kubernetes in production. Most of those systems are built from services that talk to each other, and every one of them needs an answer to the question this guide is about.
Testing in microservices: why distributed systems are hard to test
Testing in microservices is hard because correctness lives in the interactions between independently deployed services rather than inside any one of them. Even a small system makes the point.
These are the microservices testing challenges teams meet first, and they appear long before the system reaches thirty services.
- Failures happen at the seams. In a monolith, a bug is usually inside a function. In microservices, the bug is usually in the contract between two services: a renamed field, a changed default, a timeout that one side expects and the other does not honor. Neither service’s own tests can see it, because each service is correct by its own definition.
- Every service is on a different version. Services deploy independently, so at any moment the system is running a mix of versions nobody tested together. A test that pins its dependencies to specific versions is testing a combination that may never exist in production.
- The network is part of the system. Calls can be slow, can fail partway through, can be retried and arrive twice. Unit tests do not exercise any of this. Integration tests only exercise it if the dependency on the other end is real.
- Data is split. Each service owns its data, so a workflow that spans services spans databases. Tests that need consistent data across services need either a shared fixture every service agrees on, or a way to give a test its own isolated data without copying every database.
- Async flows have no call stack. When services communicate through Kafka or a queue, a producer’s test cannot see what a consumer did with the message. Testing synchronous and asynchronous flows covers how to follow a message through a broker.
- Workflows outlive deploys. A durable workflow can start on one version of the code and finish on another, retrying steps along the way. Testing durable workflows covers what that means for testing.
- Many teams share one environment. A test that fails on an environment twenty other changes are using could have been broken by any of them, so every failure starts with an investigation of whose it is.
The common thread is that correctness moved out of any one service and into the relationships between them. That is why the environment a test runs against, and how current its dependencies are, matters more for microservices than for anything that came before.
The testing pyramid for microservices
The microservices testing pyramid has five layers rather than the classic three: unit, contract, integration, end to end, and production validation. The two extra layers exist because two new failure modes appear between “the unit works” and “the whole system works”. A service’s API can drift from what its callers expect, and a release can behave differently under real traffic than under any test traffic.
| Layer | What it checks | What it runs against | What it misses | Typical tools |
|---|---|---|---|---|
| Unit | A single function or class in isolation, with no network calls | Nothing external, only in-process fakes | Whether two services still agree on how to talk to each other | JUnit, pytest, Go testing, Jest |
| Contract | That a service's API or messages still match what its consumers expect: request shapes, response fields, status codes, event payloads | A recorded specification of the other service, not the running service | How the service behaves under real conditions, past the shape of its responses | Pact, Specmatic |
| Integration | Real calls and messages between services at the versions deployed | Live dependencies at their current versions, most reliably on a shared cluster with request routing | Full user journeys spanning the interface and every downstream service at once | Language test frameworks, Testkube to run them in the cluster |
| End to end | A complete user journey across every service it touches, treating the system as a black box | A fully deployed system | Little, but slowly and expensively, and a failure rarely says which service caused it | Cypress, Playwright, Selenium |
| Production validation | That a release behaves correctly under real traffic | Production, on a slice of real users | Nothing, but it finds problems after users have already met them | Canary deploys, feature flags, synthetic checks |
Two things follow from the table. First, the layers are not interchangeable. A thousand unit tests say nothing about whether the orders service still returns the field the checkout service reads, and a passing contract test says nothing about what happens when the real orders service is slow.
Second, the integration layer carries more weight for microservices than the pyramid’s shape suggests. Interactions are where distributed systems fail, so the layer that tests interactions against real services is the one that catches the most expensive bugs. Keeping that layer current is the hard part, because an integration test is only as good as the dependencies it runs against. Is the testing pyramid broken? argues the case that the classic shape undersells it.
Three kinds of test cut across the layers instead of sitting on one. Performance, chaos and security tests each run at several of them, from a benchmark in a unit test to a load test in production, and the section on them below covers where each one pays off.
How to test microservices step by step
You test microservices in four stages: unit and local integration checks while the code is in the editor, integration tests against live dependencies at the pull request, end-to-end and performance tests before release, and validation under real traffic in production. Each stage catches a different class of problem, and each one depends on the stage before it having done its job.
1. Local development: test the one service you changed against real neighbors
Start with unit tests for the logic you touched. They run in seconds, need no infrastructure, and should be the first thing that fails when you break something.
The harder question at this stage is what to do about dependencies. Running fifty services on a laptop stops working past a handful of services, and Docker Compose files that try to recreate the system fall out of date as fast as the system changes. The alternative is to run only the service you are changing locally and connect it to a shared Kubernetes cluster that already runs the current version of everything else.
Telepresence and mirrord do this by intercepting traffic between your local process and the cluster. A lightweight ephemeral environment on the cluster does it by tagging your test requests so the cluster sends them to your local service and everything else to the shared copies. The local development on Kubernetes guide compares the options in detail.
Whatever the mechanism, the goal at this stage is the same: find integration problems while the code is still in your editor, when the fix costs minutes rather than a CI round trip.
2. Pull request and CI: run integration tests against current dependencies before merge
Once a change is in a pull request, CI should run the unit suite and then the integration tests for the services the change touches. The second part is where most pipelines fall short. CI runners start in a clean environment with no dependencies. Teams either mock everything, which loses the point of an integration test, or defer integration testing to staging, which moves the feedback to the slowest and most contended point in the pipeline.
The fix is to give each pull request an isolated place to run against live dependencies. A lightweight ephemeral environment deploys only the changed services alongside a shared cluster and routes test traffic through them, while every other call falls through to the shared versions. The Kubernetes sandbox guide explains the mechanism. Integration tests run against real services at their current versions, in parallel across every open pull request, without one full environment per pull request.
This is also where contract tests belong if you use them. A contract test checks that your service’s API still matches what its consumers expect, and it can run in CI without any dependency being live. It catches a narrower class of bug than an integration test, but it catches it earlier and cheaper.
3. Before release: end-to-end and performance tests on the paths that matter
Before release, run the thin end-to-end suite for the journeys that carry revenue or trust, and a performance check on the paths the change touched. Both need the whole system, not one service, so the environment they run in decides how much a pass is worth. End-to-end testing microservices and performance testing below cover how to run each and where.
4. Production: validate under real traffic and limit the blast radius
Some behaviors only appear under production traffic: real data shapes, real concurrency, real third-party latency. Production validation is the top of the pyramid because it is the only layer that sees them.
Canary deployments route a small slice of traffic to the new version and compare error rates and latency against the current version before widening. Feature flags separate deploying code from enabling it, so a change can ship dark and be turned on for one percent of users. Synthetic checks run scripted journeys against production on a schedule so a regression surfaces before a user reports it.
None of these replace the earlier layers. They are the safety net for what the earlier layers could not see, and a team that leans on them to catch integration bugs is paying for those bugs in production incidents. Feature flags versus preview environments goes deeper on where flags help and where they hide problems.
Integration testing for microservices
Integration testing checks that two or more real services behave correctly when they call each other. Microservices integration testing is the layer that catches the seam failures described above, and the layer where the choice of what to test against makes or breaks the result.
Mocks versus live dependencies
A mock stands in for a dependency and returns canned responses. Mocks are fast, deterministic, and free of infrastructure, which is why WireMock and MockServer are on most teams’ shortlists. They are the right tool for unit and component tests, and for third-party APIs you cannot call from a test at all, which mocking third-party APIs in an ephemeral environment covers.
They are the wrong tool for verifying integration between your own services, for a reason that has nothing to do with the mocking library. A mock encodes what one team believed the other team’s API did on the day the mock was written. The other team keeps shipping. Nothing in the pipeline updates the mock, and nothing fails when the mock and the real service diverge.
The tests keep passing against a version of the dependency that no longer exists, and the first environment to run the real pair is the one that breaks. Integration tests pass with mocks but staging still breaks walks through this failure in detail.
Contract tests narrow the gap without closing it
Contract tests are the disciplined middle ground. The consumer publishes what it expects from the provider, the provider checks it can meet those expectations, and a broker tracks which versions are compatible. They catch breaking API changes before merge and run without a live dependency.
What they do not catch is behavior. A provider can satisfy every contract and still return the wrong data, time out under load, or fail on the third retry. Contract tests check the shape of the conversation, and integration tests against the live service check the conversation itself, so most teams that get this right run both. The complete guide to contract testing covers consumer-driven and provider-driven contracts and the tool landscape in full, and the guide to AI-powered contract testing covers how the contract layer is changing as agents write more of the code on both sides of an API.
Where the live dependencies come from
If integration tests need live dependencies at current versions, something has to provide them. There are three models, and they differ in cost, in how far they drift, and in what they require of your services.
| Model | How it supplies live dependencies | Where it breaks | Prerequisites | Best fit |
|---|---|---|---|---|
| Shared staging environment | One deployed copy of every service that all teams test against | Becomes a queue past a few teams, and drifts from production once keeping it in sync is nobody's job | A CD pipeline that deploys to it | Under ten services with one or two teams |
| Full-stack ephemeral environment per change | A fresh copy of every service for every pull request | Each copy costs as much as staging, takes as long to build as the whole system, and starts drifting the moment it is created | Namespace or cluster automation, seeded data per copy, budget that scales with pull requests | Small systems, or changes that touch most services at once |
| Lightweight ephemeral environment on a shared cluster | Deploys only the changed services and routes everything else to the shared copies already running | Needs a routing header propagated through every service, and stateful side effects isolated deliberately | Header or trace-context propagation across services, a proxy or service mesh | Ten or more services, multiple teams, or coding agents opening pull requests |
The third model is the one most teams past a certain size land on. It is the only one whose environment stays current on its own. There is one shared set of dependencies, the CD pipeline already keeps it current, and every change gets its own isolated slice of it.
The prerequisite is real, though. If a service drops the routing header on the way through, requests behind it fall back to the shared copies silently, and the test passes against the wrong version without saying so.
End-to-end testing microservices
End-to-end testing microservices means driving one complete user journey through every service it touches and checking the outcome the user sees, with the system treated as a black box. It is the only test that proves the services work together as a product, and the most expensive one to run and to debug, so the suite should stay small and the environment under it should be trustworthy.
How to test microservices end to end
- Pick the journeys. Choose the handful that carry revenue or trust, such as sign-up, checkout, a payment and a refund. Leave edge cases to integration tests, which fail closer to the cause.
- Drive them from the outside. Use Playwright or Cypress from the UI, or an API client against the public gateway when the journey has no UI.
- Own the test data. Each run creates the users, carts and accounts it needs and tags them, so two runs never read each other’s records.
- Wait for outcomes, not for time. When a step goes through a queue or a workflow, poll for the result with a deadline instead of sleeping a fixed number of seconds. Fixed sleeps are a common source of flaky end-to-end tests in event-driven systems, because the right wait changes with load.
- Carry a trace ID. Start every journey with a trace context, so a failure points at the service and the span where it broke instead of at the whole system.
- Run against one change at a time. A failure on a shared environment could belong to any change deployed there. A failure in an environment that holds one change belongs to that change.
Where to run end-to-end tests
The traditional answer is a staging environment: one deployed copy of the whole system that every team’s changes pass through before production. Staging works until the number of teams sharing it turns it into a queue, and until the effort of keeping it in sync with production lets it drift. Once it drifts, a passing staging run stops meaning much.
The alternative is an ephemeral environment per change, and there are two ways to build one. A full-stack ephemeral environment copies every service for every pull request, which removes the queue but multiplies the cost and gives each copy its own chance to drift. A lightweight ephemeral environment deploys only the changed services onto a shared cluster and routes everything else to the current copies already running there, so every change gets an isolated slice of one current environment. Whichever you choose, environment parity with production is the property to protect.
Testing synchronous and asynchronous flows in microservices
Synchronous and asynchronous flows fail in different ways, so they need different tests. A synchronous REST or gRPC call fails while the caller waits, and the error comes back up the chain. An asynchronous flow through Kafka, SQS, RabbitMQ or Pub/Sub fails after the producer has moved on, so the producer’s test passes and the failure shows up later, in another service.
Most systems use both, often in the same user journey. The guide to how microservices communicate covers the protocols themselves. This section covers how to test each kind of flow.
Testing synchronous call chains: REST and gRPC
A synchronous chain fails at the seam between two services: a renamed field, a changed status code, a timeout one side sets shorter than the other side’s retries. Four checks catch most of it.
- Schema compatibility before merge. For gRPC,
buf breakingcompares a Protobuf schema against a past version and fails the build on a breaking change. For REST, a consumer-driven contract or an OpenAPI diff does the same job. - The real chain at the pull request. Send requests through the changed service and the real services it calls, at their current versions. Timeouts, retries and wrong values in correctly shaped fields show up here and nowhere earlier.
- Failure paths, not only the happy path. Make a dependency slow or return errors and check that the caller times out before its own caller does, retries with backoff, and never retries a call that is not idempotent. Chaos testing covers how to inject those faults without hurting anyone else.
- Header propagation. Trace and routing headers have to survive every hop. A service that drops them breaks tracing in production and breaks request routing in a test environment. grpcurl is the quickest way to call a gRPC service by hand with the headers set.
Testing asynchronous flows: Kafka, SQS, RabbitMQ and Pub/Sub
Testing async microservices starts from one fact: an asynchronous flow has no call stack to follow. The producer’s test can only prove that it published. Whether the consumer parsed the message, handled a duplicate, or processed messages out of order shows up somewhere else, later.
- Assert on the effect, not the publish. Publish a real message to a real broker and assert on what the consumer did: the row it wrote, the event it emitted, the call it made. Poll for it with a deadline.
- Test redelivery and ordering on purpose. Deliver the same message twice and out of order. Brokers do both in production, and an idempotency bug only shows when they do.
- Check message contracts. Pact supports message pacts for asynchronous messages, and an AsyncAPI document, now at version 3, describes channels and payloads the way OpenAPI describes endpoints.
- Keep each test’s messages apart. On a shared broker, a consumer under test will happily process another team’s messages, and the stable consumer will process the test’s. Either give the test its own topic or queue, or tag each message with a routing key so only the consumer under test picks it up.
The routing-key approach keeps one broker for everyone. Producers carry the key in message headers, and each consumer version processes only the messages meant for it. Message isolation for Kafka, SQS and more shows how that maps onto each broker. The guide to testing Kafka-based microservices compares cluster, topic and message-level isolation and walks through a working setup, and there are hands-on versions for SQS, RabbitMQ and Google Pub/Sub. Testing event-driven architectures covers the same ground at the architecture level.
| Synchronous: REST, gRPC | Asynchronous: Kafka, SQS, RabbitMQ, Pub/Sub | |
|---|---|---|
| How it fails | In the call, while the caller waits | After the producer has moved on, in another service |
| What the producer's own test proves | The response it got back | Only that it published |
| Typical bugs | Renamed fields, changed status codes, timeouts, retry storms | Parse failures, duplicates, out-of-order processing, poison messages |
| Contract check | buf breaking for Protobuf, Pact or an OpenAPI diff for REST | Pact message pacts, AsyncAPI documents |
| What to assert on | The response, its latency and its errors | The consumer's effect, polled with a deadline |
| Isolation per change | Route requests by a header | Route messages by a header, or a topic or queue per change |
Testing durable workflows
Durable workflow engines make multi-step processes survive crashes and restarts by recording each step and replaying the record after a failure. That is why teams adopt them, and it is also what makes them harder to test than a plain service. A workflow outlives deploys, retries steps on its own, and depends on code that behaves the same way every time it replays.
Why durable workflows add testing complexity
- Replay has to be deterministic. The engine rebuilds a workflow’s state by replaying its recorded history through the workflow code. A change that alters the order of steps can break every workflow already in flight, and no test of a single request sees it.
- Workflows outlive deploys. A workflow started on Monday may still be running when Thursday’s version ships. The test that matters is old history against new code, plus new code against the services its steps call today.
- Retries are automatic. The engine retries a failed step without asking, so a step with a side effect that is not idempotent charges twice, sends two emails or writes two rows.
- Time is part of the logic. Timers, deadlines and sleeps of days have to be tested without waiting days.
- Steps call real services. A workflow is a sequence of calls to other services, databases and queues, so its integration test has the same dependency problem as any other.
How to test services that use a durable workflow engine
Test them in two steps, and run the first one everywhere.
- The workflow logic, with the engine’s own tools. Workflow SDKs generally ship a test environment that runs workflows against a local test server and skips time, so a workflow that sleeps for a week finishes without waiting a week. Most also replay recorded histories against new code, so a change that would break workflows already in flight fails the build. Both run in CI with no cluster.
- The changed code against real services. Signadot treats the workflow engine like any other shared dependency, the way it treats a database or a broker, so the engine stays shared. Work started by the change carries its routing key, the changed worker picks up only that work, and the current workers handle everything else. The steps the changed code runs call the shared services, routed by the same key, and a step that writes data can get its own database or schema for the change. Nothing another team is running is affected.
The Temporal workers tutorial walks through the second step end to end, with workers in Python, TypeScript, Go and Java sharing one task queue.
Testing microservices with real databases, queues and workflows
Mocks miss the bugs that live in state and in asynchronous handoffs: a migration that breaks a service still on the old schema, a consumer that misreads a renamed field, a workflow step that runs twice on retry. Catching them takes the real database, broker and workflow engine in the test, with each change’s use of them kept apart from everyone else’s.
Why mocks miss async and stateful bugs
A mock answers the question it was written for. These bugs come from questions nobody wrote down.
- Schema and data. A migration adds a required column while another service, not yet redeployed, still inserts rows without it. An in-memory fake accepts the insert. The real database rejects it.
- Messages. A producer renames a field in an event, and the consumer’s tests still pass against last quarter’s sample payload. Brokers redeliver and reorder, which a stubbed queue never does. The section on asynchronous flows covers both.
- Durable workflows. A durable workflow retries and replays steps on its own, so a step that is not idempotent fails on the second attempt that a mock never makes.
- Batch and scheduled jobs. A nightly job processes whatever the online services wrote that day, so its test is only as good as the data and messages it runs against.
Contract tests catch part of the message case when a team publishes schemas for its events. None of the four shows up in a unit test.
How to give each change real state without copying everything
A full copy of every database, broker and workflow cluster per pull request is the slowest and most expensive part of a full-stack environment, and most changes do not need it. There are three ways to keep a test’s state apart, from cheapest to most isolated:
- Share the stable copy and partition it. Tests write rows under their own tenant or user and clean up after themselves. This covers most changes that read data or add rows.
- Share the broker or task queue and route by key. Producers and workflow starters attach the change’s routing key, and the consumer or worker under test handles only work carrying it while the stable versions handle the rest. No second Kafka or Temporal cluster is needed.
- Give the change its own copy. A schema change or destructive test gets a temporary database, schema or branch, created when the environment starts and deleted when it ends. The same goes for a topic, queue or bucket the change needs to itself.
The ephemeral environments guide covers each mechanism in detail. A Signadot ephemeral environment uses all three. It duplicates only what changed or needs isolating and links it to the rest: it routes requests by key, routes messages and Temporal tasks by the same key, and changes a forked service’s configuration to point it at its own database or queue. Resource plugins create those per-change resources, in the cluster or outside it, such as managed databases, queues and buckets, and delete them with the environment. How Bitso branches a Neon database for every environment shows the database case in production use.
Performance, chaos and security testing for microservices
Performance, chaos and security tests check properties of the whole system rather than a feature: how fast it is under load, how it behaves when a dependency fails, and whether one service can reach data it should not. In microservices each of those properties lives in the calls between services, so each test needs real services to call.
| Test | What it finds in microservices | Where it runs | Tools |
|---|---|---|---|
| Performance and load | Latency that adds up along a call chain, a new loop that calls a downstream service once per item, connection pool limits, consumers falling behind a topic | Per change against the changed service, and at full scale in an environment sized like production | Grafana k6, Gatling, Apache JMeter, Locust |
| Chaos | Missing timeouts, retry storms, fallbacks that never fire, one slow dependency taking down its callers | Per change for a single injected fault, and as scheduled experiments in pre-production and production | Toxiproxy, Istio fault injection, Chaos Mesh, LitmusChaos, AWS Fault Injection Service, Gremlin |
| Security | Broken authorization between services, internal APIs open to any caller, vulnerable images, misconfigured clusters | Static checks in the editor and CI, dynamic tests against running services | Semgrep, Trivy, kube-bench, ZAP |
Performance and load testing microservices
Performance testing microservices has two jobs that need two different setups. The first is catching regressions: did this change make the checkout path slower? That test is small, runs on every pull request against the changed service and the real services behind it, and compares the result with the shared version of the same service measured at the same time. Compare relative numbers rather than fixed thresholds on a shared pre-production cluster, because the dependencies under both versions are shared and their load moves.
The second job is capacity, which is what most writing on microservices performance testing means: how much traffic the system takes before it degrades. That test needs an environment sized like production with nothing else running on it, and it runs on a schedule or before a launch, not on every change.
In microservices a regression rarely sits inside one service. Latency adds up along a call chain, a new loop calls a downstream service once per item, or a consumer falls behind its topic, so the test has to run through the real chain to see it. Grafana k6 takes scripts in JavaScript or TypeScript and has a gRPC client built in. Gatling, Apache JMeter and Locust, which takes scenarios in Python, cover the same ground. Running scalable k6 tests against a pull request’s version of a service walks through the per-change setup.
Chaos testing microservices
Chaos testing injects failures on purpose, such as latency, errors, dropped connections or a killed pod, to check that the system degrades the way its design says it should. For microservices the useful questions are concrete. Does the caller time out before its own caller does? Does it retry with backoff or all at once? Does the fallback ever fire?
Start small and per change. A fault injected between the changed service and one dependency, with a TCP proxy such as Toxiproxy or the mesh’s own fault injection in Istio, tests the change’s timeout and retry handling in minutes. Scope matters on a shared cluster. Inject the fault only on the path of the change under test, or every other team’s tests fail with it.
Cluster-wide experiments, such as killing pods or cutting off a zone, belong in scheduled runs in pre-production and in production game days. Chaos Mesh and LitmusChaos, both CNCF incubating projects, run them in Kubernetes, and AWS Fault Injection Service and Gremlin offer managed versions.
Security testing microservices
Microservices security testing adds a question a monolith never had to ask: which service may call which, with what data? The first risk on the OWASP API Security Top 10, last revised in 2023, is broken object level authorization, an API that returns any record it is asked for and trusts the caller to have checked. Inside a cluster, internal APIs are often called with no authorization at all, on the assumption that only trusted services can reach them.
Most of the work is automated and runs early: static analysis such as Semgrep in the editor and CI, image and configuration scanning with Trivy, and kube-bench to check the cluster against the CIS Kubernetes Benchmark. Dynamic tests need a running system. ZAP, the open source scanner now backed by Checkmarx, probes a service’s API from the outside, and authorization tests call each service as the wrong user and as the wrong service and expect a refusal. Those tests mean most against real services, where the mesh’s mutual TLS and authorization policies apply the way they do in production.
What AI coding agents change about testing microservices
Coding agents change microservices testing in two ways: they multiply the number of changes that need verifying, and they need that verification to happen without a person in the loop. Testing AI-generated code in a microservices architecture is the same problem as testing any other change, run at a volume and cadence the old pipeline was not built for.
The volume is measurable. GitHub’s own account of its August 2026 outage puts merged pull requests at about 130 million a month, well over double the monthly rate it reported for 2025, and Claude Code, Cursor, Codex, and GitHub Copilot all open pull requests on their own now. None of that volume removes a bottleneck that already existed in microservices testing. It multiplies the changes arriving at each one, and every one of those pull requests touches services whose neighbors it cannot see.
Agents are also consumers of the test environment, not only producers of changes. An agent works in a build-test-fix loop: write the change, run it against its dependencies, read the failure, fix, repeat. That loop cannot wait for a shared staging slot or a fifteen-minute environment build. An agent without a fast integration environment falls back to unit tests and mocks, which is exactly the layer that cannot see the seams.
Verification has to happen before human review, not after. A reviewer’s time is the scarce resource once agents generate the code, so the integration and end-to-end tests that used to run after approval need to run before the pull request reaches a person. The pull request arrives with evidence that the change worked against real dependencies, or it does not arrive at all.
Each agent run needs its own isolation. Two agents testing conflicting changes to the same service on one shared environment corrupt each other’s results, so every run needs an isolated slice of the dependency graph, created and destroyed on the agent’s timeline. That also means cost and lifetime controls: a time-to-live on every environment, and a cost model that scales with changed services rather than with the number of agents running.
An agent reaches the environment through the same interfaces a developer uses. It can run the CLI, call the API, or use a Model Context Protocol (MCP) server that exposes environment creation and test execution as tools it invokes directly, and which of the three it uses depends on where the agent runs. Validating AI-generated code against real Kubernetes dependencies covers that path end to end, including how an agent reads a failed integration run and iterates on it.
Do you need a staging environment to test microservices at all?
You need what staging is for: a shared environment where a change meets the current versions of everything it depends on before users do. What stops working past a few teams is handing that environment to one change at a time. The “staging is dead” argument is right that one shared copy of the system turns into a queue, and wrong when it concludes that mocks, contracts, and canaries can replace the integration layer entirely.
Staging as one shared copy is still the right answer for small systems. Under about ten services with one or two teams, conflicts are rare, drift is visible because everyone can see the whole system, and the cost of one shared copy is trivial. Replacing it with per-change environments at that size adds machinery without removing a problem.
Past that size, keep the shared environment and change how changes use it. Every change still runs against live dependencies at current versions, but each one gets an isolated slice of the shared environment rather than a turn on it. Kubernetes staging environments covers how teams make that move without a flag day.
Building a microservices testing strategy
A microservices testing strategy is the decision about how much to invest at each layer of the pyramid and where the integration layer gets its live dependencies. Most microservices testing strategies come down to four questions, and one principle organizes the answer.
How many services, and how many teams own them?
Below about ten services with one or two teams, a shared staging environment and a thin end-to-end suite usually work. Conflicts are rare and drift is manageable because everyone can see the whole system.
Past that, staging becomes a queue and the drift becomes invisible, because no one team owns the whole picture any more. This is the point where integration testing needs an isolated place to run per change, and where the choice between copying environments and sharing one becomes the central architecture decision in the strategy.
How do your services talk to each other?
A system built mostly on synchronous calls puts its weight on integration tests of call chains and on contract checks for each API. A system built on Kafka, a queue or a workflow engine needs message and workflow tests at the pull request too, plus chaos tests for redelivery and retries, because that is where its failures hide. Map the flows first, then decide which synchronous and asynchronous tests each one needs.
Where do failures come from?
Pull the last quarter’s production incidents and sort them: bugs inside a single service, failures at the seam between two services, and configuration or environment differences. Teams that do this usually find the second and third categories dominate while their test investment is still weighted toward the first.
If that is your result, shift effort up the pyramid: fewer redundant unit tests and more integration tests against live dependencies. Add a hard rule that every pull request runs its integration tests before merge rather than after.
How fast is the change rate growing?
A pipeline sized for today’s pull request volume will be undersized within a year for most teams, and within months for teams adopting coding agents. Design for the volume you expect, not the volume you have.
The cost model decides whether that is affordable. A full-stack environment per change scales cost with the number of pull requests. A lightweight ephemeral environment on a shared cluster scales cost with the number of changed services per pull request, which is usually one or two regardless of how many pull requests are open.
Shift-left testing for microservices
Shift-left testing means running each layer of the pyramid at the earliest stage that can support it: unit tests in the editor, integration tests at the pull request, end-to-end tests before release rather than after. For microservices the shift that pays most is moving integration tests from staging to the pull request, because that is where seam failures are cheapest to fix and where a queue of changes waiting on one environment forms first. Shift-left testing for microservices works through which layers move, what the pull request environment has to provide, and the two ways to build it.
How do you know the strategy is working?
Measure it with the four DORA metrics: lead time for changes, deployment frequency, change failure rate, and time to restore. A testing strategy that moves integration tests to the pull request should show up as shorter lead time and a lower change failure rate within a quarter, because the seam failures that used to surface in staging or production now surface before merge.
If change failure rate does not move, the tests are still running against the wrong dependencies. If lead time does not move, the environment is still a queue. How to do DORA metrics right covers how to instrument the four without gaming them.
Test automation tools vs environment platforms
Microservices testing needs two layers of tooling that are easy to confuse. Test automation tools write, run and orchestrate the tests, from unit frameworks and contract brokers to load generators and in-cluster orchestrators such as Testkube. Environment platforms give each change somewhere real to run those tests, against current dependencies and isolated from other changes, which is what Signadot, Release, Bunnyshell and Okteto do. Most teams need something from each layer, and two tools from the same layer still leave the other gap.
| Layer | The question it answers | Examples | What it leaves to the other layer |
|---|---|---|---|
| Test automation and orchestration | What do we test, and how do the tests run and report? | JUnit or pytest, Pact and Specmatic, Playwright and Cypress, k6 and JMeter, Testkube to run them in the cluster | The services under test, their dependencies, and keeping both current |
| Environment platforms | Where does this change run, against which versions of its dependencies, isolated from every other change? | Signadot, Release, Bunnyshell, Okteto, Qovery, or an in-house namespace per pull request | What to test, and whether the change works |
Where the two layers overlap
The one place the layers meet is running the tests. An orchestrator such as Testkube executes suites inside the cluster but expects the environment to exist already. Some environment platforms can start a suite too: Signadot can run one as a Job in pods next to an ephemeral environment, for teams that have nowhere else to run it. The comparison of Testkube and Signadot Jobs covers what each one does.
For a testing strategy, the decision is where results land, not which runner is better. Keep one place that runs and reports tests: CI, the orchestrator you already have, or a pod in the cluster, pointed at each change’s environment. The environment platform provides the place to run, and the tests, written by people or by agents, decide whether the change works.
Match tools to each layer of the pyramid
Most teams end up combining tools, one or two per layer of the pyramid. The survey of microservices testing tools for Kubernetes compares the field in depth, environment platforms included. In brief:
- Unit and component: the language’s own test framework, with WireMock or MockServer standing in for HTTP dependencies.
- Contract: Pact, the open source framework, with SmartBear’s PactFlow, now sold as Swagger Contract Testing, as the hosted broker, or Specmatic for teams that want to drive contracts from OpenAPI specs. Signadot versus Pact covers where contract tests stop and live integration tests start.
- Local development against a cluster: Telepresence, a CNCF Sandbox project created at Ambassador, which Gravitee acquired in 2025. And mirrord from MetalBear, which began as a way to run a local process against cluster traffic and has grown cluster-side features since. Its Team tier adds queue splitting for Kafka and SQS and database branching, and its Enterprise tier adds preview environments and CI use. Signadot versus mirrord compares the two models. So does Signadot versus Telepresence.
- Test orchestration in Kubernetes: Testkube runs existing Postman, k6, Playwright, Cypress, and pytest suites as workflows inside the cluster, against an environment you provision.
- End to end: Playwright or Cypress from the UI.
- Performance, chaos and security: k6, Gatling or JMeter for load, Chaos Mesh or LitmusChaos for fault injection, Trivy and ZAP for security, as covered in the section above.
- Where integration and end-to-end tests run: an environment platform that gives each change live dependencies at current versions, such as the lightweight ephemeral environment per change covered in the closing section.
Running the shared test environment like a product
Whichever model supplies your live dependencies, the shared environment behind it needs an owner, a definition of current, and a way to tell whose change broke what. Teams that skip this end up with the drift they were trying to escape, one layer down.
Ownership comes first. The shared set of dependencies is a platform team’s product, with a stated objective for how far behind the main branch it may fall and an alert when it does. Parity is a mechanism, not a wish: the same CD pipeline that deploys production deploys the shared environment, from the same images and configuration, so the two cannot diverge by hand.
Data and async flows need tiers. Shared read-mostly reference data can live once. Anything a test writes, publishes or starts needs the isolation described in testing with real databases, queues and workflows, or a test on the orders service ends up processing another team’s events and both results are noise. A migration under test must never reach the copy every other change depends on.
Access decides whether the environment gets used. QA, product owners, and reviewers need a URL per change. Developers need to connect a local process into the shared environment from their editor. Agents need the same through a CLI or MCP, with a time-to-live on everything they create.
When a test fails against forty real services, tracing has to attribute the failure to the one service that changed, or every failure becomes an investigation.
Microservices testing best practices checklist
Twelve yes-or-no questions. A team that can answer yes to all of them has closed the gap this guide is about.
- Does every service have fast unit tests that run in the editor and in CI?
- Do contract tests run before merge for every API and event another team consumes?
- Does every pull request run integration tests against live dependencies before a human reviews it?
- Are those dependencies at their current versions, kept in sync by the same pipeline that deploys production?
- Can two changes to the same service be tested at the same time without interfering?
- Does each change that writes data get its own isolated schema or database?
- Do async flows through Kafka, a queue, or a durable workflow engine get the same isolation as synchronous calls?
- Is your end-to-end suite thin enough that a failure is investigated the same day, and does it wait on outcomes rather than fixed sleeps?
- Do latency-sensitive paths get a performance check on every change, and critical dependencies a fault-injection test for timeouts and retries?
- Is authorization between services tested, rather than assumed because the caller is inside the cluster?
- Can developers and coding agents create, test against, and destroy an environment on their own, with a time-to-live on each one?
- When an integration test fails, does tracing point at the one service that changed?
How lightweight ephemeral environments keep every test running against current dependencies
A lightweight ephemeral environment keeps tests current by giving every change its own isolated copy of only the services it touched, running against the shared versions of everything else, so integration and end-to-end tests always meet current dependencies. Signadot is a mature implementation of this pattern, and the microservices testing solution page covers how teams adopt it.
The mechanism, in plain words: duplicate only what changed or needs isolating, link it to the rest through request routing, message routing and configuration changes, and manage the lifecycle through a CLI, an API, or MCP. One Kubernetes cluster runs the current version of every service, kept current by the same CD pipeline that deploys production. When a change needs testing, an ephemeral environment deploys only the changed services alongside them and expires on a timer once the change merges or is abandoned.
Test requests carry a routing key in a header. A service mesh or lightweight sidecar reads that key and sends the request to the forked service, while every other call falls through to the shared copy. The same key travels through message queues and Temporal task queues for async flows, and an environment that touches data can carry its own database or schema so its writes never reach the shared copies. Resource plugins create anything else a change needs to itself, inside the cluster or outside it. Versions of services that run outside Kubernetes, on ECS, Lambda or virtual machines, are coming soon.
An integration test for a two-line change therefore runs against every other service at its current version without deploying those other services. Spin-up takes seconds because only the changed service is deployed, and cost scales with changed services rather than total services. Brex runs pre-merge environments this way for hundreds of engineers and saves about $2 million a year in infrastructure compared with duplicating environments. Drift stops being a problem because there is one environment, and the CD pipeline already keeps it current.
That is the property the rest of this guide has been circling. When every test runs against dependencies that are current, the distance between a passing test suite and a working production system gets small enough to trust. It stays that small as the number of services and the number of authors keep growing.
Where to go next
If you run fewer than ten services and staging still works, read the staging environments guide and keep it healthy. If staging has become a queue, start with the ephemeral environments guide and the comparison of environment architectures above. If most of your flows run through Kafka or a workflow engine, start with asynchronous flows and durable workflows above. If agents are opening your pull requests, start with validating AI-generated code against real Kubernetes dependencies, because the pipeline you build for them is the pipeline everyone else ends up using.
Related reading
- Contract Testing: The Complete Guide
- Best Microservices Testing Tools for Kubernetes in 2026
- Integration tests pass with mocks but staging still breaks: how to catch it earlier
- How Microservices Communicate with Each Other
- Testing Microservices: Message Isolation for Kafka, SQS, More
- Testing Kafka-Based Microservices in Kubernetes: The Complete Guide
- Tutorial: Test Temporal Workers on Kubernetes
- How to Run Scalable Performance Tests Using Grafana/K6 and Signadot
- Is The Testing Pyramid Broken?
- Shift-Left Testing for Microservices on Kubernetes
- Local Development on Kubernetes: The Complete Guide
- Validating AI-Generated Code Against Real Kubernetes Dependencies
- Microservices Testing for FinTech
- Kubernetes Testing: The Complete Guide
Frequently asked questions
What is microservices testing, and how is it different from testing a monolith?
Microservices testing checks that each service works on its own and that services keep working when they call each other over the network or hand work off through queues and workflows. Testing a monolith is mostly about logic inside one codebase, where a changed signature breaks the build. Between services the same break happens at runtime, across versions owned by different teams, so more of the effort moves to integration.
What is the difference between microservices testing and integration testing?
Integration testing is one layer inside microservices testing, not a separate practice. Microservices testing covers unit, contract, integration, end-to-end, performance, chaos and security tests together. Integration testing checks specifically that two or more real services behave correctly when they call each other, at the versions actually deployed. Contract testing sits between the two and checks API and message shapes without running the other service.
What types of tests does a microservices application need?
Seven kinds, in different amounts. Unit tests cover logic inside a service. Contract tests check API and message shapes. Integration tests run real services together. End-to-end tests drive a whole user journey. Performance tests find slow call chains, chaos tests check behavior when a dependency fails, and security tests check authorization between services. In microservices testing, the last five only mean something against real dependencies.
How do you test microservices end to end?
Keep the suite thin and run it against real services. Pick the few user journeys that carry revenue or trust, drive each through the UI or the public API with Playwright, Cypress or an API client, and run it in an isolated environment for each change instead of a shared one. Poll for asynchronous outcomes with a deadline instead of sleeping. End-to-end testing microservices this way leaves one change to blame for a failure.
How do you test synchronous and asynchronous flows between microservices?
Test a synchronous REST or gRPC call by sending a request through the real chain of services and checking the response, the latency and the error handling. Test an asynchronous flow by publishing a real message to a real broker and asserting on what the consumer did. In microservices testing both need isolation per change: request routing for calls, and message routing or a separate topic or queue for Kafka, SQS, RabbitMQ and Pub/Sub.
How do you test microservices that use a durable workflow engine?
In two steps. First test the workflow logic with the engine's own tools: a test server that skips time, and a replay of recorded histories against new code to catch changes that would break workflows already in flight. Then run the changed workflow code against the real services its steps call. Microservices testing that stops at the first step misses the retry, replay and versioning bugs durable workflows are prone to.
Why do microservices tests pass but production still breaks?
Because the tests ran against dependencies that no longer match what runs in production. Mocks go stale as the services they imitate change, and a staging environment three deploys behind gives the same false confidence. The fix in microservices testing is to run integration tests against the current shared versions of dependencies rather than against static mocks or a copy of the system that has drifted.
How many environments do you need to test microservices safely?
Three layers, not a separate dev, QA, staging and pre-production environment apiece. You need a local or agent working copy of the one service being changed, one shared cluster holding the current versions of every other service, and a way to give each change its own isolated slice of that cluster. That is enough for microservices testing at any number of services, because one cluster serves every change at once.
Can a coding agent test a microservices change against real dependencies before it opens a pull request?
Yes, if it has somewhere to run the test. An agent works in a build-test-fix loop and needs an isolated environment with real dependencies it can create, test against, and tear down without waiting on a person. Given a CLI, an API or an MCP server that exposes those actions, agents like Claude Code and Cursor run microservices testing before a reviewer ever sees the change.
How do AI coding agents change microservices testing requirements?
They raise the volume of changes and remove the person from the loop. Every agent run needs its own isolated environment with live dependencies, a time-to-live so abandoned runs clean themselves up, and integration results attached to the pull request before human review. Microservices testing for agent-written code is the same set of layers as before, run per change at a cadence a shared staging environment cannot keep up with.