Component Tests for Spring Cloud Microservices
In this tutorial, we discussed guidelines and considerations for Spring Cloud microservices component tests and provided a recipe for common use cases.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
The shift towards microservices has a direct impact on the testing strategies applied and has introduced a number of complexities that need to be addressed. In fact, microservices require additional levels of testing since we have to deal with multiple independently deployable components.
An excellent explanation of these concepts and the various levels of microservices testing is given by Martin Fowler in his Testing Strategies in a Microservice Architecture presentation. Let's have a look at the revised "test pyramid" from this presentation:
In this tutorial, we will dive into component tests: In a microservice architecture, the components are the services themselves. By writing tests at this granularity, the contract of the API is driven through tests from the perspective of a consumer. Isolation of the service is achieved by replacing external collaborators with test doubles and by using internal API endpoints to probe or configure the service [1].
In the sections below we will present a component testing recipe for a sample Spring Cloud microservice.
The Service
Our sample Spring Boot microservice will have the following characteristics:
- Will be Spring Cloud Netflix enabled, i.e. it will use the Netflix OSS integrations for Spring Boot apps to perform service registration and discovery, distributed/external configuration (Spring Cloud Config), and client-side load balancing
- Will integrate with a relational database (PostgreSQL)
- Will perform a call to another microservice (internal)
- Will perform a call to a third party web service (external)
- Will have Spring Security enabled (will act as an OAuth2 resource server)
- Will be "hidden" behind an API gateway server, like Spring Cloud Gateway
We will use Java 11, Apache Maven, and Docker with a set of collaborating libraries that will give us the possibility to test the service in isolation, as early as possible in a CI/CD pipeline, without the need to actually deploy or spin-up other services, databases, or occupy the resources of a complete test environment.
All the code for this demo is published online on GitHub.
Our "order tracking" microservice consists of one Spring Controller, Service, and Repository. It exposes two endpoints:
- GET /api/orders/{trackingNumber}/status: Given a tracking number, a DB query is executed to fetch the related order, then an internal service call to a FulfillmentService to determine the delivery status, and depending on the status, a final external service call to a LocationService, to determine the location. This is a protected API call and a valid JWT must be in place
- GET /api/orders: This lists all orders by querying the database. It's also a protected API call, this time with an additional authorization restriction — it is available only for users with the role "back-office."
The Component Test
The class OrderControllerTest.java will encapsulate our component tests against the two methods offered by our API.
Now, there are many ways to separate and categorize unit tests, integration tests, component tests, contract tests, etc. using Maven plugins, JUnit features, Spring Boot test slices, naming conventions, and of course proper scripting on your CI server. Typically not all test categories are executed (or re-executed) during a CI/CD pipeline. In this demo, we keep it simple, but in a real-world scenario, you are strongly encouraged to implement a proper categorization.
Our test configuration properties in /src/test/resources/application.yml are the following:
server:
port: 0
spring:
application:
name: order-service-test
cloud:
service-registry:
auto-registration:
enabled: false
loadbalancer:
ribbon:
enabled: false
config:
enabled: false
jpa:
show-sql: true
eureka:
client:
enabled: false
service-url:
registerWithEureka: false
okta:
oauth2:
issuer: https://kmandalas/oauth2/default
location-service:
url: http://localhost:9999/v1/track/
In the above file, notice that we have disabled spring.cloud.config
, eureka.client
, and spring.cloud.service-registry.auto-registration
. This is because we are testing our microservice in isolation. Therefore there will be no Spring Cloud Config server present to serve the configuration properties of our OrderService on startup nor will there be a Eureka server to register with and be able to use it for dynamic service discovery of the FulfillmentService, which we need to invoke.
The Database
When having to integrate with a database (relational or NoSQL) for the purpose of testing, theoretically you have three options:
- Go with an embedded or in-memory solution (H2 for example)
- Use an actual database that is accessible during tests
- Use an ephemeral database that is close or even identical to the production one
There are many resources out there analyzing why options one and two are not the best, taking into consideration of course the test phase. For example, if someone selects H2 for integration and/or component tests, then he/she would have to maintain separate DDL and DML scripts since most likely the production DB will be different than H2. Also, maybe native queries are used or other DB-specific features that make this choice a bad one. On the other hand, if we are talking about end-to-end or performance tests, etc., then an actual deployed database should be used, which will be up and running in a test environment. In this case, modern IaC (infrastructure as code) tools along with careful test data management can provide the flexibility needed depending on the project.
For our particular test phase, we will go with option three, utilizing testcontainers and Flyway, which play together nicely with Spring Boot. The database is PostgreSQL. With the help of testcontainers, a temporary dockerized database instance will be created at the beginning of test context initialization and Flyway will trigger the execution of our migration scripts (DDL, DML) on this temporary schema. Then our code will transparently run against this temporary schema and when the tests finish, the dockerized DB will be disposed.
All we need here is the @Testcontainers
annotation on our OrderControllerTest
class along with the following static declarations:
@Container
static PostgreSQLContainer database = new PostgreSQLContainer("postgres:12")
.withDatabaseName("tutorial")
.withUsername("kmandalas")
.withPassword("dzone2022");
@DynamicPropertySource
static void setDatasourceProperties(DynamicPropertyRegistry propertyRegistry) {
propertyRegistry.add("spring.datasource.url", database::getJdbcUrl);
propertyRegistry.add("spring.datasource.password", database::getPassword);
propertyRegistry.add("spring.datasource.username", database::getUsername);
}
The Internal Service Call
We use Spring Cloud OpenFeign for invoking the FulfillmentService, which is supposed to be within our data center, i.e. another "internal" Spring Cloud microservice which is expected to be registered with Eureka. Under normal execution, the feign client behind the scenes is locating the target service instances by name and does client-side load balancing (if more than one instance is discovered).
During our test phase and in the absence of Eureka (or another discovery mechanism such as Consul), we need two things to simulate this integration as realistically as possible:
- WireMock for spinning up a mock server that intercepts our requests based on URL patterns and replies with the mocked responses we have provided
- A
@TestConfiguration
that will emulate the discovery of FulfillmentService instances and will point to WireMock server's URI. You can have a look at this test configuration here.
As an embedded mock server, we could use Hoverfly, but we prefer introducing WireMock and more specifically via the following dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
This is because with spring-cloud-starter-contract-stub-runner
, the bootstrapping of WireMock in a Spring Boot application test suite is simplified plus it is very useful for another very important category of tests, namely the contract tests. For more information, check Spring Cloud Contract WireMock.
With all the above in place, all we need to do is to annotate our test class with @AutoConfigureWireMock
and define some WireMock mappings in a JSON file under our test resources directory.
The External Service Call
For this integration, we will also rely on WireMock. We want to invoke an external (third party) service so we need valid WireMock mappings (the more variety of responses we provide, the better) and of course a test URL like the one we define in our test resources application.yml:
location-service:
url: http://localhost:9999/v1/track/
Notice here that we provide the host and the port that WireMock embedded server is expected to run followed by our external service's URL endpoint path. The port doesn't have to be hard-coded but it can be defined as dynamic. This way there will be no port collisions if we run multiple component tests in parallel during our CI/CD pipeline.
One more thing to note is that WireMock can be used for mocking not only JSON responses from RESTful services but the responses of SOAP-based web services, as well!
The Security
As we mentioned above, it is common for a Spring Cloud microservices infrastructure to incorporate an API gateway like Spring Cloud Gateway. This provides various options for handling security. We will go with the Token Relay pattern which is supported by OAuth 2.0, JavaScript Object Signing and Encryption (JOSE), and JSON Web Tokens standards. This will give our users the means to identify themselves, authorize applications to view their profile, and access the secured resources behind the gateway. A very common setup, in this case, consists of the following components:
- A single sign-on server, like Keycloak, Cloud Foundry’s User Account and Authentication Server, or VS commercial OAuth2 authentication providers like Okta
- An API gateway server, such as the Spring Cloud Gateway which delegates the management of user accounts and authorization to the single sign-on server
- Resource server(s): our Spring Boot microservice(s) like the OrderService in our example
Since in this test phase we are testing our Spring Boot microservice in isolation, we will use Spring Security’s SecurityMockMvcRequestPostProcessors
. This will give us the ability to pass valid JWTs and define authorities (i.e. user roles) during our MockMvc
calls and test the behavior of our component with the security enabled. For example:
mockMvc.perform(get("/api/orders/11212/status").with(jwt())).andExpect(status().isOk());
and
mockMvc.perform(get("/api/orders/").with(jwt().authorities(new SimpleGrantedAuthority("backoffice"))))
.andExpect(status().isOk());
Wrap-Up
A popular quote in the CI/CD jargon says that...
The secret to success is to... "fail fast!"
Therefore it is vital for a successful delivery to include a well balanced mix of various categories of tests that will be executed both by the developers and in an automated fashion during a CI/CD pipeline. In this tutorial, we focused on Spring Cloud microservices component tests and provided a recipe for some common use cases out there. Happy testing!
Opinions expressed by DZone contributors are their own.
Comments