Microservice Integration Problems — Here’s How to Fix Them
One of the hardest problems when working with microservices is the difficulty in maintaining backward compatibility with existing clients of your services, when introducing changes to the API or implementation.
There are two aspects to this problem:
First, as service developers we would like to know if changes that we introduce to our code are going break existing clients. These include changes to the API as well as to the behavior of the code implementing the API.
Secondly, as consumers of a service we would like a way to guarantee that our understanding of the API and the protocol for communicating with the service are correct.
In both cases we would want to find out at build time when problems are less expensive to correct — not in production.
FakeServices to the Rescue
a FakeService is a lightweight implementation of a microservice. It can be used as a test utility for consumers who want to verify their code integrates well with your service, without needing to deploy it and all of its dependencies locally, on the consumer’s machine.
For example — if my service stores data in a database, a fake version of my service would store that same data in a map, in memory. Consumers of my service would now be able to start-up the FakeService in their test environment and not worry about installing a database, or any other external dependency, the service may depend on.
In order to be useful, a FakeService needs to conform to the same contract as the real service.
They behave the same from the outside but use a simplified implementation of the behavior, without executing external calls to other services.
Usually FakeServices will run on a local thread within the test process and started before the System Under Test (SUT) starts.
For instance, let’s assume I’m testing code that executes calls against some external service. I would write a test that starts-up the provided FakeService, start-up the SUT pointing it to the local port on which the FakeService is listening. I would then execute calls against the SUT through its public API and assert whatever outcome I expect to happen.
The SUT will unknowingly communicate with the FakeService as if it was communicating with the real one.
As a service consumer it can benefit me in at least 2 ways:
- I can verify that I’m calling the external API correctly.
I can find out immediately if I’m passing all the right parameters and calling the right endpoints on the external system.
- It ensures that the code implementing the external service behaves as I expect. For instance — that the inputs I’m sending to the API are passing validation rules and that I react correctly to the responses.
How to Implement a FakeService?
In order for FakeServices to be effective two conditions need to be met:
Firstly, the FakeService must be provided by the vendor of the real service and not implemented by the consumer.
Second, the FakeService must behave exactly like the real service.
How would we build a FakeService that is guaranteed to behave exactly like the real service it is emulating?
Contract tests are tests that can run against both the real service and the FakeService. They describe the contract for the behavior of the service.
I like to use a technique I learned from J.B Rainsberger for writing these tests: I write the tests in an abstract base class.
These tests would call abstract factory methods that would provide the concrete instance of the SUT.
The test for the real service would start-up the service and all its dependencies, while the test for the FakeService would instantiate the fake with no other external dependencies.
Both test classes would extend the abstract base class containing the contract tests and therefore when running the build, the test runner would run the contract for both the real service and the fake.
This ensures that they both conform to the exact same contract.
As a service provider, it’s safe to assume that if my contract tests are breaking as a result of introducing a change to the code, then — there’s a good chance this will also break my consumers running in production.
Therefore the contract tests are also regression tests for the service owner, who doesn’t want to break their clients.
The following example is taken from petri — Wix.com’s open source A/B testing framework.
Ports and Adapters Architecture
In order to easily implement the contract tests, the FakeService reuses parts of the business logic of the real system. This can be achieved by adhering to the Ports and Adapters architectural style.
Implementors of a a FakeService can reuse the same core logic as the real service and provide a simple in-memory implementation of the adapters.
To wrap it all up, as service providers we are responsible for maintaining our side of a contract with our users. We can use Contract Tests to explicitly specify that contract. Using contract tests will enable us to find out if we’re about to introduce a breaking change at build time.
We can also implement a FakeService, which should be easier to do if we design our code in a way that is decoupled from the context in which it is running. The FakeService will need to pass the exact same contract tests, and will be deployed in a test-kit module.
Users of our service can use the FakeService in their own test environment and can assure their code integrates well with ours.
Hi I’m Sagy Rozman.
I’ve spent the last 15 years writing code, leading agile software projects , speaking in meet-ups and conferences, running workshops and teaching developers about TDD, clean code and software craftsmanship.
I currently work as a technical coach at Wix.com
You’re welcome to follow me on twitter