End-to-End Microservice Tests with Cucumber
Table of Contents
If you work with Microservices, you surely faced the situation in which some of your system processes only make sense after the information flows through several microservices. For instance, if you’re implementing an Event-Driven architecture, you should verify your Business Processes end-to-end, including the complete event chain that spans across services. There are several frameworks you can use for that but, for its simplicity and perfect matching with Business requirements, Cucumber is a great option. Let’s get into the process of writing end-to-end scenarios using Cucumber.
NOTE: this post doesn’t cover the use of Dependency Injection with Cucumber in Spring Boot. However, I’ve posted another article with a code sample to explain how to achieve that: Cucumber Tests in Spring Boot with Dependency Injection
What is Cucumber?
Cucumber is a framework intended for Behavior-Driven-Development (BDD) that supports many different programming languages. BDD is an approach that defends writing first the requirements in human language (Behavior) and, only then, developing the code making it cover the described behaviors. To accomplish this, Cucumber created Gherkin, a DSL that the framework can interpret.
Gherkin builds a powerful bridge between Business Users -using this language to describe the requirements- and Developers -implementing the code behind every sentence of that human-understandable language. Let’s see an example of a Gherkin test definition:
How does it work?
The idea is that we organize our features into multiple
.feature files (like the one above). In each of them, we include the description of the feature at the top of the file. Cucumber ignores the description, it’s just part of the documentation. Then, every feature has multiple scenarios, which technically are your different test case definitions. Finally, each scenario is defined by multiple steps, using the BDD keywords: Given, When, Then (plus And and But).
Each scenario (or test case definition) runs within its own context. This concept is very important to understand to implement our tests correctly. We can share the state between steps of the same scenario, but not across scenarios. For instance, we can save values returned by some steps and use them later to verify the whole flow, but we can’t use those values within a different scenario.
The steps can be parameterized using arguments. We can reuse the same step definition in multiple scenarios with different values, or we can also pass a data table to the same scenario. If we pass a data table, each row is called example.
Mapping Scenarios to Code
Let’s use this step to understand how arguments work:
Gherkin itself does not have any tagging to specify which words are arguments. We define that at the code level. In this case, we’d like to pass to our step the user alias, the number of attempts and if they are right or wrong. Let’s have a look at how this step needs to be implemented in Java to support our arguments:
We’ll have a look at the coding details in the following section. For now, you see that we only need to annotate our step method and use some regular expressions. Two word-expressions and a numeric one will do the trick in this particular case.
Combining Cucumber with other tools
Now that we know how to link the Features to Java code, how do we continue? Keep in mind that Cucumber is not giving you a full testing framework for Java, it’s just the link between your test definitions and code. To make our scenarios work, we should include in our project some other useful tools. In our case, we’ll introduce:
- JUnit: since it’s giving us the core testing support for Java.
- AssertJ: I prefer its way of asserting results, but you could also use the JUnit style.
- Apache Fluent HttpClient: because we want to keep it lightweight and at the same time have a straightforward way to perform requests to our microservices.
- Jackson 2: we want to (de)serialize JSON requests/responses.
Note that, even though we’ll use Spring Boot for our microservices, we don’t need to use it for our test system.
Coding the solution with Java and Cucumber
It’s time to get into the code. I’ll use an existing microservices application, the one included in my book’s Learn Microservices with Spring Boot repository. If you’re wondering how the application works you can benefit from Cucumber and read the feature included above (cool!).
The best way for you to get a good understanding is to clone or download that repository and experiment with it (don’t forget to give it a star!).
So, let’s assume you have the microservices application up and running. We want to check the different cases in which the user posts HTTP requests to one of the services (via UI and the gateway). Then, that service triggers an event. Finally, a different microservice will consume it and perform some extra operations. This is a typical End-to-End microservice test suite, exercising an Event-Driven architecture.
The E2E project is structured as shown in the picture:
- Our JSON expected responses are modeled as simple beans (
- The main bridge with the application is the
MultiplicationApplicationclass. We use it to make our requests, etc., from the functional perspective (e.g. send an attempt instead of send HTTP POST).
- The technical implementation to contact with our app is developed in
ApplicationHttpUtils, which uses Apache Fluent HTTP.
Then we have the main classes modeling our tests (we have two features thus two tests):
MultiplicationFeatureTest. They are just entry points for the Cucumber runner to find the right
.feature file, which are placed in the
resources folder. We use two annotations here:
@RunWith to use the Cucumber runner, and
@CucumberOptions to specify the feature we want to execute and the reporting capabilities. We want to generate a JUnit-compatible report.
Having understood all this, the only piece we miss is the implementation of the steps. We can complete the full story now and check how the steps trigger the Java methods within a fresh context per scenario, and how assertions make their part.
For the curious ones, two notes:
- Did you notice the thread pausing at some point of our test? It’s there because our system is Eventually Consistent. The second microservice needs some time to consume the event and complete the operation. The
sleep()methods are there just to illustrate the eventual consistency so make sure you don’t use those waits in a production-code test. They will make your test flaky (e.g. if you have a congested RabbitMQ server). Instead, you should implement a retry mechanism and a longer timeout.
- If you have a look at the
LeaderboardFeatureTest, you’ll see how Cucumber steps can be reused across features using the
cucumber-picocontainerdependency. It’s pretty straightforward to understand: one of the beans in the (pico)context is injected in the other one.
Note that this approach tests the microservices from an external project that is not using Spring Boot. If you want to see an example of how to use Cucumber inside a Spring Boot application (e.g. for Integration Tests inside the service) you can have a look at this other post: Cucumber Tests in Spring Boot with Dependency Injection