What are Microservices?
To increase the frequency of releases and reduce the burden of deployments, many companies have moved from a monolith to microservice architecture. Spotify, Amazon, and Netflix have the benefits of successful implementations of microservice architectures.
A monolithic architecture combines the user interface, business logic, and data interface into a single application
A Micro Service architecture is a method of developing software applications as a suite of independently deployable, small, modular services in which each service runs a unique process and communicates through a well-defined, lightweight mechanism to serve a business goal.
The benefits of Microservices are Strong Module Boundaries, Independent Deployment, and when implemented correctly, Scalability.
- Fault tolerance and network latency. When testing monoliths typically the entire application is up or down. With microservices different services can be up or down, for example, a user can successfully login as the login service is up but the user account service is down.
- The emergent behavior of Microservices makes it challenging to predict behavior in a synthetic staging environment i.e. Independent services communicating in many (often unanticipated) ways
- Recoverability, when a service was down and then becomes available, what behavior do we expect?
- Distributed data, in the monolith world, all data is typically stored in one database, how do we manage data when is spread across databases linked to different services.
- Communication between contexts/teams. Different services must communicate with each other in a coherent manner and more importantly, teams must communicate how their contexts interact.
- Increased complexity makes troubleshooting more challenging, due to the above risks, troubleshooting a problem can be significantly more challenging. Thus Testability Observability must become a top priority.
The Microservice Test Levels
So far we have seen there can be huge benefits from Microservices but there are inherent risks and new challenges.
We can mitigate using many approaches clearly defining the tests levels
In simple terms, we could say that there are 5 levels and each level focuses on a specific layer
The diagram below illustrates these 5 levels.
Unit: individual pieces of a service
This is the smallest piece of testable software. An important decision to be made is whether to mock or not, these two approaches are known as Black Box(sociable) and Test Doubles(solitary). Typically a team will compare the value the tests brings versus the cost or constraints.
From a test perspective, it’s important that the unit tests do not just focus on technical implementation or code coverage metrics. Microsservices should be simple and typically serve a business goal, so think about what Business logic can be verified at this level.
2. Component: a specific service
This is where we test the service, typically through an endpoint. At this level, there is another important decision to be made, whether to perform In process (quick iteration tests) or Out of process (more realistic deployment-style tests).
For In Process, the internals of the service are altered so that it has a start up test mode. There are no network calls and you have a mix of test and app code but it is fast.
For out of Process, a test service is created. The complexity is in the test microservice, there are network calls but execution time increases as you are starting a server.
As per the unit tests, typically a team will compare the value the tests brings versus the cost or constraints.
From a test perspective, a significant range of test types can be performed at this level.
- Basic functional testing.
- Fault-tolerance testing, you can simulate failures and test increased response latency.
- Performance testing can commence.
- Basic security testing.
- Visibility testing, endpoints for metrics and health checks can be verified, as well as logging and alerting.
3. Contract: between consuming and producing services
Next, we need some tests between an API provider and an API consumer. Managing the interactions between Microservices has been described as trying to herd cats.
Typically teams will try three approaches end to end testing, mocking and consumer-driven approach.
In end-to-end tests (E2E tests), a whole runtime environment is setup. But testing performed at this level will be slow, brittle and difficult to troubleshoot.
The mock approach approach typically involves avoiding the set up of a whole runtime environment, but run isolated tests between the consumer and a mock provider and between a mock consumer and the real provider. We now avoid the overhead of a whole runtime environment but we now have two sets of tests instead of one. We all know there is a cost to maintaining tests, so do we really want two test suites.
The Consumer-driven approach is a another forming of mocking, the consumer defines what it expects from a specific request to a service. The provider and the consumer agree on this contract. The provider continuously verifies that the contract is fulfilled. We now avoid the overhead of a whole runtime environment and we have just 1 set of tests. Most importantly we now have an approach that fits nicely into a continuous integration pipeline with our unit and component tests.
There some other benefits of consumer-driven tests
- Peace of mind for providers
- Confidence for consumers
- Trustworthy, Cheap, Fast and Targete
4. Integration:between your services and other services or data stores
Up to this point based on our approach, we could have “mocked” up through the first three levels, we may not have integrated with our storage solutions or integrated with third party services. (If you have then the team may decide that this layer can be skipped).
At this layer, we consider if we need a small amount of “real” integration tests, these are best suited to being executed in a runtime environment. Thus may be suited to the continuous delivery rather than continuous integration aspect of your pipeline.
5. End to End: the end to end flow through the application
End-to-end tests demonstrates that a system can achieve its desired purpose and best suited to being executed in a runtime environment.
For an ecommerce website microservices you may want to test in an end to end flow may include login, add an available product to the cart, navigate to the checkout page, select payment method and ensure confirmation of purchase.
Traditionally we would have tested the end to end flows through the UI. Although it’s possible to test this entire flow through the API, this opens opportunities for automation that is faster, easier to maintain and more reliable than UI tests.
The UI Test Levels
Now that we have covered the Microservice testing we can now consider the UI testing.
We could describe three test levels for the UI Layer.
1. Unit tests
2. Contract: between consuming and producing services
Similarly to the API, the UI layer needs a contract between the consumer and producer.
The end to end flows/user flows can still be implemented at the UI layer but you now have a range of test levels that you trust and thus you needs less of these brittle, slow tests. So there can be more of a focus on key user flows.
The Test Funnel for Microservices and UI
So we now have 4 or 5 test levels for the Microservice and 3 levels for the UI layer.
Traditionally we may have viewed these levels on a pyramid digram like this.
Although the test levels are more clear when displayed as a funnel
The Microservices Deployment Pipeline could look something like below
- Unit, Componets and Contract test can run on a build server as part of your continuous integration process
- The Integration, End to End Tests and Security Testing could be run on a Pre-Production Environment
- Performance testing is best suited to a dedicated environment or perhaps a docker image.
- Monitoring, Alerting, Synthetic Test and Analytics all play a part post deployment of the Microservices.
Exposure to testing Microservices has exposed me to both their challenges and benefits, and made me realize their is so much to learn. Breaking down the Test Levels helps us to analyse risk and focus on what testing we want to perform.