Welcome to Part 3 of my adventures through Spring Boot. Thank you for returning to follow along with my journey learning how to transition my Ruby-colored glasses to more Java tinted ones. Next, I’ll be looking at writing functional testing capabilities to mirror RSpec, self-documenting code with Swagger, integrating metrics into the app, and how to validate all of these changes.
RSpec lends itself to a more Behavior-Driven Development style of testing. Since I’m more comfortable with testing in that manner, I’ll be using tools that mirror that functionality, so tests read more like spoken word. These will also look more like Integration tests to folks because they will use real PostgreSQL containers and real API calls to the application to perform tests.
SOME OF THE RELEVANT TOOLS WE’LL BE USING
I wanted to include a list of tools you’ll see added as well as some existing tooling being used here:
Testing Libraries
Rest-Assured Integration testing
Swagger OpenAPI generation
Metrics
STARTING UP
To start, we need to update our pom.xml with the libraries we need, so I’ve added the excerpts that are new for this post (not the whole xml file).
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
...
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${io.restassured.version}</version>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured-all</artifactId>
<version>${io.restassured.version}</version>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-path</artifactId>
<version>${io.restassured.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.18.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${io.restassured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured-common</artifactId>
<version>${io.restassured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>${io.restassured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>[2.2,)</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>[2.2,)</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>[2.2,)</version>
<scope>test</scope>
</dependency>
We also need to add functionality for database migrations in the pom.xml in a particular order, as Maven loads plugins in a special order and will break if not done correctly. We are adding the following Flyway JARS:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>[7.5,)</version>
</dependency>
...
<build>
<!-- ADD OR MOVE THIS PLUGIN FIRST -->
<!-- ******* WARNING: ORDER MATTERS FOR PLUGIN EXECUTION ***************** -->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals><goal>copy</goal></goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>[7.5,)</version>
<destFileName>flyway.jar</destFileName>
</artifactItem>
<artifactItem>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.driver.version}</version>
<destFileName>postgres.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
Next, we move to the GeneralDataConfig.java to allow Flyway to manage migrating the Postgresql Database. We are also adding an additional class so that migrations can be run on-demand from the Docker Image as a one-off. To make it all come together, we also need the DDL of our database in SQL format for Flyway to use. You can export the DDL to an initial file in the Flyway format in src/main/resources/db/migration/V0001__schema_ddl.sql
. This works by convention in Flyway as the default path and format. Incremental changes can be made such as V0001.1__update_indexes.sql.
# GeneralDataConfig.java
public class GeneralDataConfig {
// Default to false for environment deployments to not auto-migrate
// set to true for local test suite
@Value("${spring.flyway.enabled:false}")
public Boolean FLYWAY_ENABLED;
@Value("${flyway.createSchemas:true}")
public Boolean CREATE_SCHEMAS_FLAG;
@Value("${flyway.defaultSchema:public}")
public String DEFAULT_SCHEMA;
// comma-separated list of names, can be asked to separate schemas.
// Not Recommended to use multiple schemas without a specific business need
@Value("${flyway.schemas:public}")
public String SCHEMAS_NEEDED;
# ...
# in final Properties additionalProperties()
hibernateProperties.setProperty(Environment.HBM2DDL_AUTO, "validate"); # changes from "update"
# ...
@Bean(initMethod = "migrate")
@DependsOn({"dataSource"})
Flyway flyway(DataSource dataSource) {
Flyway flyway = Flyway.configure()
.dataSource(dataSource)
.defaultSchema(DEFAULT_SCHEMA)
.createSchemas(CREATE_SCHEMAS_FLAG)
.schemas(SCHEMAS_NEEDED)
.baselineOnMigrate(true)
.target(MigrationVersion.LATEST)
.load();
return flyway;
}
# Migrations.java in src/main/java/com/demo/fulfillment
package com.demo.fulfillment;
import java.util.ArrayList;
import java.util.List;
import org.flywaydb.core.Flyway;
public class Migrations {
public static void main(String[] args) {
String[] connInfo = getDatabaseConnectionInfo();
Flyway flyway = Flyway.configure().dataSource(connInfo[0], connInfo[1], connInfo[2]).load();
// Matches functionality of https://flywaydb.org/documentation/usage/maven/
switch (args[0]) {
case "clean":
flyway.clean();
break;
case "info":
flyway.info();
break;
case "baseline":
flyway.baseline();
break;
case "validate":
flyway.validate();
break;
case "repair":
flyway.repair();
break;
default:
flyway.migrate();
}
}
protected static String[] getDatabaseConnectionInfo() {
List<String> connInfo = new ArrayList<>();
// Example JDBC_DATABASE_URL
// jdbc:postgresql://{host}:{port}/test?user={user_name}&password={password}
String dbUrl = System.getenv("JDBC_DATABASE_URL");
// Standard URI parser doesn't work with Postgresql URI format
// See https://gist.github.com/BillyYccc/fb46baefab59d7aa2f0dc11d28e6f0e1
String[] userParams = dbUrl.split("\\?")[1].split("&");
connInfo.add(dbUrl);
connInfo.add(userParams[0].split("=")[1]); //user
connInfo.add(userParams[1].split("=")[1]); //pass
return connInfo.toArray(String[]::new);
}
}
Controller Testing
Now that our data layer has been hardened with Database Migrations, we can write full end-to-end integration testing through the controllers in Spring. Below is an example using RestAssured to validate JSON serialization and deserialization.
# CustomersITest.java
package com.demo.fulfillment.controllers;
import com.demo.fulfillment.BaseITest;
import com.demo.fulfillment.models.Customer;
import com.demo.fulfillment.services.CustomerService;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class CustomersITest extends BaseITest {
@Autowired
CustomerService service;
@AfterEach
public void cleanup() {
service.deleteAllCustomers();
}
@Test
public void testGet_returns_200_with_array_Customers() {
List<Customer> customersList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
customersList.add(Customer.builder().email("customer_" + i)
.firstName("Townsperson" + i)
.lastName("Simpson")
.addressOne("100" + i + " Sherry Lane")
.city("New York")
.state("NY")
.zipCode(10024)
.build());
}
service.addCustomers(customersList);
List<Customer> actual = given
.log()
.all()
.when()
.get( "/customers")
.then()
.assertThat()
.statusCode(200)
.extract().body().jsonPath().getList("", Customer.class);
assertThat(actual).isNotEmpty();
assertThat(actual.size()).isEqualTo(10);
}
}
Lastly, we’re adding the ability for our Prometheus and our OpenAPI specifications autogenerated from our controllers to be presented with SpringDoc with a hosted Swagger UI.
# add the following to GeneralDataConfig.java
@Bean
public GroupedOpenApi actuatorApi() {
return GroupedOpenApi.builder()
.group("actuator")
.pathsToMatch("/actuator/**")
.build();
}
@Bean
public GroupedOpenApi fulfillmentApi() {
return GroupedOpenApi.builder()
.group("fulfillment")
.packagesToScan("com.demo.fulfillment.controllers")
.build();
}
# in FulfillmentApplication.java
# add the following annotation for OpenAPIDefinition
@OpenAPIDefinition(
info = @Info(title = "Fulfillment"),
externalDocs = @ExternalDocumentation(
url = "https://github.com/genslein/fulfillment-demo"
),
servers = {
@Server(url = "/")
}
)
@SpringBootApplication
public class FulfillmentApplication
# ...
Now we have a fully functional interface with premade queries ready to validate with a real Postgresql DB backing our services. Just run docker-compose up -d
and navigate to the Swagger UI http://localhost:8080/swagger-ui/index.html?configUrl=/v3/api-docs/swagger-config.


Thank you very much for joining me on my adventure. If you like what you saw, feel free to reach out with suggestions on what you’d like to see me cover next.