There is a lot of buzz about microservices these days, and here is our take on managing decoupled microservices, while still keeping confident in REST API compatibility.
One of the SOA missteps was trying to enforce a canonical domain model, which lead to bloated domain objects, that still would not fit all use cases and needed some sort of central management. Just try to come up with a common model of any real-world object and you’ll know that this does not scale the more people are involved. In a more abstract sense, it is our strive to perfection and the strive of perfect modelling of our problem domain that drove us into the wrong direction. David Dawson phrased this as “Perfection and How It Ruined The World” in his talk “Philosophical Architecture in Grails” He also illustrated brilliantly why monoliths are doomed to fail ultimately.
Client SDKs would be just another way of a shared domain model, and additionally would force the consumer to be using one specific client library. This brings in tight coupling, which should be avoided, unless all consumers are under your own control. In that case gRPC might be an interesting alternative to using REST.
So, instead of managing a globally shared domain model, slowing down our development, we would rather let every service define its own view of the domain model in order to achieve loose coupling. Coupling, as Oliver Gierke correctly stated, is not binary.
Still surprised people consider decoupling to be a binary thing. If you don't want no coupling at all, don't let systems talk to each other.
— Oliver Gierke 🥁&👨💻 (@olivergierke) May 20, 2016
Of course you need communication between your services, and of course this introduces some coupling. But, when opening an API to external consumers, it becomes a shared concern that you need to manage. And it implies, that the producer cannot introduce breaking changes without the possibility of breaking the consumer’s API integration. It actually doesn’t matter if it is asynchronous communication or synchronous communication via REST. In both cases you need to share a common API “vocabulary”. And, you need to verify the contract defined within the API.
There are three ways of dealing with it. Let’s start with the most obvious, but most brittle one: Integration testing. Let’s test our microservices in integration, allowing us to verify the complete system behaviour from an outside point of view. This of course requires all services and their infrastructure to be up and running simultaneously. Think of integration tests failing mysteriously every few weeks…
We replaced our monolith with micro services so that every outage could be more like a murder mystery.
— Honest Status Page (@honest_update) 7. Oktober 2015
A better way would be trying to test your client against actual responses of the server without the server having to be up and running. How can you achieve this? Interestingly, the solution is not too hard. Martin Fowler calls this test method “Integration Contract Testing”. For integration contract testing the producer is replaced with a “test double” and pre-recorded responses would be replayed for the test scenario.
It is one level before “Consumer-driven contract testing”, where the consumer provides its test scenarios to the producer, which allows checking consumer compatibility even before introducing a potentially breaking API change. But this has the down-side of off-loading all the testing effort to the producing side.
Martin Fowler still sees the “[..] danger that the test doubles get out of sync with the real supplier service”. How can we avoid getting out of sync, ideally without extra effort? First of all, we shouldn’t let the client define the test double’s responses. And, we make sure the pre-recorded responses are shared artifacts, which get updated whenever we release a new version of the producer.
There is another artifact which should never become out of date with the implementation: Our API documentation. Why not kill two birds with one stone and produce both human-readable REST documentation and API stubs directly as part of testing your producing service? For doing this we came up with a small utility library, that we decided to share as open source on GitHub. It combines the goodness of Spring REST Docs for producing documentation, and WireMock, a tool for mocking a webserver during testing. In the end, the WireMock stubs become just another part of our service documentation, which happen to be machine-readable.
Let’s look at some code. Assume you have a note-collection service written with Spring WebMVC, and you want to document your API with Spring REST Docs, which would look similar to this code:
@RunWith(SpringJUnit4ClassRunner.class)
class ApiDocumentation {
// ... the usual test setup.
void should_retrieve_single_note() {
this.mockMvc.perform(get("/notes/1")) // (1)
.andExpect(status().isOk()) // (2)
.andDo(document("get-note", // (3)
responseFields( ... ), // (4)
wiremockJson() // (5)
));
}
}
In the example above we would test retrieving a given note via our REST API (1) and first validate the response (2) from the server. Next, we document the findings (3), generating request and response snippets, that we can include in our API documentation. This also asserts that all fields within the communication (4) are documented. If we, for example, introduce a new response field and forget to document it, this test would start failing. Finally, we save the input and output of the communication as WireMock stub (5).
When you’re at that point, it becomes trivial to publish the WireMock stubs into your artifact repository to share them with your consumers. On the consumer side you need to pick up those stubs by including them on the test class-path and loading them into a locally running WireMock server. We have a dedicated Spring Boot Starter that simplifies this process. Ultimately it boils down to one additional annotation on the test case.
@RunWith(SpringJUnit4ClassRunner.class)
@WireMockTest(stubPath = "wiremock/notes-service") // (1)
public class NoteServiceConsumerTest {
@Value("http://localhost:${wiremock.port}/") // (2)
String wiremockUrl;
}
In the code example we annotate our consumer test with an annotation to start a WireMock server along with our test environment (1). By default WireMock gets started on a random port, and we need to point our API consumer to it, for example by using a Spring expression (2). WireMock would happily serve all pre-recorded responses from the producer, given the same inputs and resource paths.
Happy microservice testing!