DEV Community

Rubem Vasconcelos
Rubem Vasconcelos

Posted on

From Monoliths to Microservices: Rethinking the Test Pyramid

In the previous article, I explored the fundamentals of software testing and the classic Test Pyramid.

This post builds on that foundation and is adapted from Chapter 4 – Tests in Microservices of my Master’s thesis, where I analyze how testing strategies evolve when moving from monolithic architectures to distributed microservices systems.

The core idea is simple:

The Test Pyramid still applies — but its structure must evolve in distributed systems.


Testing in Monolithic Systems

Monolithic systems are deployed as a single unit. All components share the same codebase, runtime, and memory space.

This architectural simplicity has direct implications for testing.

Test Pyramid
Image Source: Martin Fowler, The Practical Test Pyramid

It usually includes:

  • Unit Tests
  • Integration Tests
  • End-to-End (or UI) Tests

Unit Tests

Unit testing focuses on validating small, isolated parts of the system. As Fowler (2014) emphasizes, unit tests should be fast and independent, forming the foundation of the test suite.

Because everything runs within the same process, isolation is easier and execution is deterministic.


Integration Testing

Integration testing combines components and verifies their interactions.

According to ISTQB (2018), integration testing aims to expose defects in interfaces and interactions between integrated components or systems.

Two common forms include:

  • Component integration testing – interaction between internal modules
  • System integration testing – interaction with external systems (e.g., third-party APIs)

In monolithic systems, integration remains relatively predictable because communication occurs within the same application boundary.


End-to-End Testing

System testing evaluates the fully integrated system against specified requirements (ISTQB, 2018).

End-to-end testing simulates real user flows, validating complete business scenarios across the application stack (Fowler & Lewis, 2014).

Since monoliths are deployed as a single artifact, validation of full flows is operationally simpler than in distributed architectures.


What Changes With Microservices?

Microservices architectures introduce:

  • Independent deployments
  • Network communication
  • Asynchronous messaging
  • Distributed data management
  • Cross-team ownership
  • Runtime unpredictability

Failures no longer occur only inside the codebase — they happen between services, across network boundaries.

Bozkurt, Harman, and Hassoun (2010) observed that distributed systems introduce higher testing complexity due to:

  • Dynamic service behavior
  • Simultaneous distributed transactions
  • Multiple implementations under shared specifications
  • Cross-team coordination

This complexity forces a structural evolution of the testing strategy.


The Microservices Test Pyramid

Microservices Test Pyramid
Image Source: SOTOMAYOR et al. (2023)

In addition to traditional:

  • Unit Tests
  • Integration Tests
  • End-to-End Tests

Microservices introduce two critical layers:

  • Component Tests
  • Contract Tests

In many models, integration, component, and contract tests form the broader service testing layer.


Unit Testing in Microservices

In microservices, units typically include:

  • Domain services
  • Resource handlers
  • Gateways
  • Repositories

However, as Habl, Kipouridis, and Fottner (2017) note, successful isolated unit tests do not guarantee correct behavior once components interact within distributed systems.

Network boundaries fundamentally change the reliability assumptions.


Integration Testing in Distributed Systems

In microservices, integration testing focuses on:

  • Database interactions
  • Inter-service communication
  • Messaging systems
  • External APIs

Sotomayor et al. (2023) highlight that integration testing in microservices architectures must explicitly validate communication layers and service coordination mechanisms.

Common failure sources include:

  • Serialization mismatches
  • Version incompatibilities
  • Timeout configurations
  • Infrastructure inconsistencies

Integration tests must validate these boundaries intentionally.


Component Testing

Component testing treats each microservice as an independent system.

Using:

  • Mocks
  • Stubs
  • Simulated dependencies

external network interactions are removed during testing.

This ensures:

  • Deterministic execution
  • Faster feedback loops
  • Reduced flakiness

As Fowler and Lewis (2014) argue, isolating services during testing is essential to prevent infrastructure instability from compromising test reliability.

Component tests bridge the gap between isolated unit tests and full distributed integration.


Contract Testing: A Structural Necessity

One of the most important additions in microservices architectures is contract testing.

Contract testing verifies service boundaries and interaction agreements.

Sotomayor et al. (2023) define contract testing in microservices as the validation of service boundaries to ensure compatibility between independently evolving services.

In distributed systems:

  • APIs define request/response contracts
  • Messaging schemas define event contracts
  • Consumers depend on explicit interface structures

If a provider changes its contract without coordination, it may break consumers in production.

Contract testing ensures:

  • Schema compatibility
  • Behavioral consistency
  • Safe independent deployments

Contract testing addresses methodological complexity introduced by independently deployed microservices.

Tools like Pact are widely used to operationalize this practice.


Why End-to-End Tests Must Be Minimized

End-to-end testing in microservices:

  • Requires multiple services running simultaneously
  • Depends on production-like environments
  • Is slower and more fragile
  • Is harder to debug

Over-reliance on E2E tests can lead to:

  • Flaky CI pipelines
  • Slow feedback cycles
  • High maintenance overhead

Cohn (2010) already recommended minimizing top-layer tests in the Test Pyramid. In microservices architectures, this recommendation becomes even more critical.

Confidence should be built lower in the pyramid — especially at service boundaries.


Structural Differences: Monolith vs Microservices

Monolith Microservices
Failures mostly internal Failures often at boundaries
Shared memory communication Network communication
Single deployment Independent deployments
Implicit contracts Explicit contracts
Centralized coordination Distributed coordination

Testing evolves from validating internal correctness to validating communication guarantees and service contracts.


Final Thoughts

Microservices provide:

  • Scalability
  • Deployment independence
  • Team autonomy

But they introduce:

  • Greater operational complexity
  • Increased testing surface area
  • More failure modes

The Test Pyramid still applies — but its center shifts toward:

  • Strong unit tests
  • Robust service-level tests
  • Explicit contract validation
  • Minimal, strategic end-to-end tests

In distributed systems, testing strategy becomes an architectural decision.


References

  • Bozkurt, M., Harman, M., & Hassoun, Y. (2010). Testing web services: A survey.
  • Cohn, M. (2010). Succeeding with Agile: Software Development Using Scrum. Addison-Wesley.
  • Fowler, M. (2014). The Practical Test Pyramid. martinfowler.com
  • Habl, G., Kipouridis, I., & Fottner, J. (2017). Deploying microservices for a cloud-based design of system-of-systems in intralogistics. IEEE.
  • ISTQB (2018). Standard Glossary of Terms Used in Software Testing.
  • Sotomayor, B. et al. (2023). Comparison of open-source runtime testing tools for microservices. Software Quality Journal, Springer.

This article is adapted from Chapter 4 – “Tests in Microservices” of my Master’s thesis, which presents a comparative analysis of testing strategies in monolithic and microservices-based systems.

Top comments (1)

Collapse
 
matthewhou profile image
Matthew Hou

The evolution toward more integration and contract tests in microservices makes sense — unit tests that mock every boundary give you false confidence in a distributed system. Consumer-driven contract testing is the piece I see teams skip most often, but it's arguably the most valuable layer when you have multiple teams owning different services. Without it, you're back to relying on expensive end-to-end tests to catch integration issues late in the cycle.