This is a showcase for the microservice framework Spring Boot. It contains a hello world application, which demonstrates several features of Spring Boot, Spring Boot Actuator, Spring Boot Admin and Springdoc OpenAPI.
Software requirements to run the samples are maven
, openjdk-8
(or any other JDK 8) and docker
.
When running the Maven lifecycle it will use the spring-boot-maven-plugin
to create a runnable JAR (fat JAR) which contains the
application and the Tomcat server. The fat JAR will be copied into a Docker image using Spotify's dockerfile-maven-plugin
during the
package phase.
Notable Features:
- Dockerfile for runnable JAR
- Integration of Spring Boot Actuator for Health and Metrics
- Integration of Spring Boot Admin for Monitoring
- Integration of Springdoc OpenAPI
- Testcontainer tests with REST-assured, Cucumber and Postman/Newman
- Code-Coverage for Testcontainer tests
- CircleCI Integration
- Sonarcloud Integration
Before running the application it needs to be compiled and packaged using Maven
. It creates the runnable JAR and Docker image and can be
run via docker
:
$ mvn clean package
$ docker run --rm -p 8080:8080 spring-showcase
If everything worked you can access the OpenAPI UI via http://localhost:8080/swagger-ui.html and the Spring Boot Admin UI via http://localhost:8080/
Sometimes it may happen that the containers did not stop as expected when trying to stop the pipeline early. This may result in running containers although they should have been stopped and removed. To detect them you need to check Docker:
$ docker ps -a | grep spring-showcase
If there are containers remaining although the application has been stopped you can remove them:
$ docker rm <ids of the containers>
The application is a very simple "Hello World" greeting service. It supports GET requests for generating a greeting message, and a PUT request for changing the greeting itself. The response is encoded using JSON.
Try the application
curl -X GET http://localhost:8080/api/greet
{"message":"Hello World!"}
curl -X GET http://localhost:8080/api/greet/Stephan
{"message":"Hello Stephan!"}
curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Hola"}' http://localhost:8080/api/greet/greeting
curl -X GET http://localhost:8080/api/greet/greeting
{"greeting":"Hola"}
curl -X GET http://localhost:8080/api/greet/Max
{"message":"Hola Max!"}
The application provides support for health, metrics and openapi endpoints assisted by Spring Boot Actuator and Springdoc OpenAPI.
Health
curl -s -X GET http://localhost:8080/actuator/health
Metrics in Prometheus / JSON Format
curl -s -X GET http://localhost:8080/actuator/prometheus
curl -s -X GET http://localhost:8080/actuator/metrics
OpenAPI in YAML / JSON Format
curl -s -X GET http://localhost:8080/v3/api-docs.yaml
curl -s -X GET http://localhost:8080/v3/api-docs
codecentric’s Spring Boot Admin is a community project to manage and monitor Spring Boot applications. The application provides the Spring Boot Admin Client, Server and UI. The Admin UI is reachable via http://localhost:8080/.
For the application a set of integration tests is provided. The tests bases on Testcontainers combined with other testing frameworks like
REST-assured, Cucumber and Postman/Newman. The docker container for the application is build by the dockerfile-maven-plugin
during the
package
phase.
To improve the runtime of the testcontainer tests by avoid starting and stopping the container for every test class, the singleton container pattern is used.
The container is started only once when the base class is loaded. The container can then be used by all inheriting test classes. At the end of the test suite the Ryuk container that is started by Testcontainers core will take care of stopping the singleton container.
AbstractIntegrationTest - Superclass for all Testcontainers tests providing the containerized application
public abstract class AbstractIntegrationTest {
protected static final String NETWORK_ALIAS_APPLICATION = "application";
protected static final Network NETWORK = Network.newNetwork();
protected static final GenericContainer<?> APPLICATION = new GenericContainer<>("spring-showcase")
.withExposedPorts(8080)
.withNetwork(NETWORK)
.withNetworkAliases(NETWORK_ALIAS_APPLICATION)
.waitingFor(Wait.forHealthcheck());
static {
APPLICATION.start();
}
}
REST-assured is a popular test framework for testing and validating REST services that brings the the simplicity of dynamic languages into the Java domain.
To ease making HTTP requests to the containerized application, REST-assured provides specifications to reuse response expectations and/or
request parameters for different tests. The RequestSpecBuilder
is used to define the dynamic port of the application for all requests only
once.
GreetingResourceIT - Integration tests for the GreetingResource
class GreetResourceIT extends AbstractIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(GreetResourceIT.class);
private static RequestSpecification requestSpecification;
@BeforeAll
static void setUpUri() {
APPLICATION.withLogConsumer(new Slf4jLogConsumer(LOG));
requestSpecification = new RequestSpecBuilder()
.setPort(APPLICATION.getFirstMappedPort())
.build();
RestAssured.given(requestSpecification)
.contentType(MediaType.APPLICATION_JSON)
.body("{ \"greeting\" : \"Hello\" }")
.when()
.put("/api/greet/greeting")
.then()
.statusCode(Response.Status.NO_CONTENT.getStatusCode());
}
@Test
void greetTheWorld() {
RestAssured.given(requestSpecification)
.accept(MediaType.APPLICATION_JSON)
.when()
.get("/api/greet")
.then()
.statusCode(Response.Status.OK.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body("message", Matchers.equalTo("Hello World!"));
}
}
Cucumber is one of the most popular tools that supports Behaviour-Driven Development(BDD) for the Java language. Cucumber reads executable specifications written in natural language and validates that the software does what those specifications say. The specifications consist of several examples or scenarios - which is why this approach is known as Specification by Example.
Greeting.feature - Acceptance tests in natural language (Gherkin syntax)
Feature: Greeting
Scenario: Greet the world
Given a greeting "Hello"
When a user wants to greet
Then the message is "Hello World!"
Scenario Outline: Greet someone
Given a greeting "<greeting>"
When a user wants to greet "<name>"
Then the message is "<greeting> <name>!"
Examples:
| greeting | name |
| Hola | Christian |
| Hey | Max |
| Moin | Stephan |
To run cucumber tests, you still have to to use the Cucumber
JUnit4 runner, due to missing support for JUnit5.
GreetingCucumberIT - JUnit4 based test class that runs all acceptance tests of the project
@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"pretty"}, features = "src/test/resources/it/feature")
public class GreetingCucumberIT {
}
Due to its BDD-oriented nature, REST-assured seamlessly integrates with Cucumber to implement acceptance tests for RESTful APIs. To
GreetingCucumberSteps - Step definitions matching the steps in the feature file
public class GreetingCucumberSteps extends AbstractIntegrationTest {
private RequestSpecification requestSpecification;
private io.restassured.response.Response response;
@Before
public void beforeScenario() {
requestSpecification = new RequestSpecBuilder()
.setPort(APPLICATION.getFirstMappedPort())
.build();
}
@Given("a greeting {string}")
public void given_a_greeting(final String greeting) {
RestAssured.given(requestSpecification)
.contentType(MediaType.APPLICATION_JSON)
.body(new GreetingDTO(greeting))
.when()
.put("/api/greet/greeting")
.then()
.statusCode(Response.Status.NO_CONTENT.getStatusCode());
}
@When("a user wants to greet")
public void when_a_user_wants_to_greet() {
response = RestAssured.given(requestSpecification)
.accept(MediaType.APPLICATION_JSON)
.when()
.get("/api/greet");
}
@When("a user wants to greet {string}")
public void when_a_user_wants_to_greet(final String name) {
response = RestAssured.given(requestSpecification)
.accept(MediaType.APPLICATION_JSON)
.pathParam("name", name)
.when()
.get("/api/greet/{name}");
}
@Then("the message is {string}")
public void then_the_message_is(final String message) {
response.then()
.statusCode(Response.Status.OK.getStatusCode())
.contentType(MediaType.APPLICATION_JSON)
.body("message", Matchers.equalTo(message));
}
}
As expected the execution of the specification examples can also be easily followed in the log output
Cucucmber log output
[INFO] Running de.openknowledge.projects.greet.GreetingCucumberIT
Scenario: Greet the world # src/test/resources/it/feature/Greeting.feature:3
Given a greeting "Hello" # de.openknowledge.projects.greet.GreetingCucumberSteps.given_a_greeting(java.lang.String)
When a user wants to greet # de.openknowledge.projects.greet.GreetingCucumberSteps.when_a_user_wants_to_greet()
Then the message is "Hello World!" # de.openknowledge.projects.greet.GreetingCucumberSteps.then_the_message_is(java.lang.String)
Scenario Outline: Greet someone # src/test/resources/it/feature/Greeting.feature:15
Given a greeting "Hola" # de.openknowledge.projects.greet.GreetingCucumberSteps.given_a_greeting(java.lang.String)
When a user wants to greet "Christian" # de.openknowledge.projects.greet.GreetingCucumberSteps.when_a_user_wants_to_greet(java.lang.String)
Then the message is "Hola Christian!" # de.openknowledge.projects.greet.GreetingCucumberSteps.then_the_message_is(java.lang.String)
Scenario Outline: Greet someone # src/test/resources/it/feature/Greeting.feature:16
Given a greeting "Hey" # de.openknowledge.projects.greet.GreetingCucumberSteps.given_a_greeting(java.lang.String)
When a user wants to greet "Max" # de.openknowledge.projects.greet.GreetingCucumberSteps.when_a_user_wants_to_greet(java.lang.String)
Then the message is "Hey Max!" # de.openknowledge.projects.greet.GreetingCucumberSteps.then_the_message_is(java.lang.String)
Scenario Outline: Greet someone # src/test/resources/it/feature/Greeting.feature:17
Given a greeting "Moin" # de.openknowledge.projects.greet.GreetingCucumberSteps.given_a_greeting(java.lang.String)
When a user wants to greet "Stephan" # de.openknowledge.projects.greet.GreetingCucumberSteps.when_a_user_wants_to_greet(java.lang.String)
Then the message is "Moin Stephan!" # de.openknowledge.projects.greet.GreetingCucumberSteps.then_the_message_is(java.lang.String)
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.916 s - in de.openknowledge.projects.greet.GreetingCucumberIT
Postman is an popular API client that supports automated API testing. Test collections developed in Postman can be exported and integrated with your CI/CD pipeline by using Newman, Postman's command line Collection Runner.
Newman allows you to run and test a Postman Collection directly from the command line. It is built with extensibility in mind so that it can easily integrate it with continuous integration servers, build systems and even Testcontainers.
To automate Postman test collections with Testcontainers the newman docker image is required. The collection and the environment file has to be copied to the docker image, and a file system bind has to be configured, to be able to access the test reports.
IMPORTANT: The newman container is started and stopped for the execution of a single command - running the collection. To prevent that
the containers is stopped before the test collection is executed, a OneShotStartupCheckStrategy
with a timeout of 5 to 10 seconds has to
be configured for the newman container.
GreetingPostmanIT - Newman container that runs a Postman collection against the containerized application.
class GreetingPostmanIT extends AbstractIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(GreetResourceIT.class);
private static final GenericContainer<?> NEWMAN = new GenericContainer<>("postman/newman:5.1.0-alpine")
.withNetwork(NETWORK)
.dependsOn(APPLICATION)
.withCopyFileToContainer(MountableFile.forClasspathResource("postman/hello-world.postman_collection.json"),
"/etc/newman/hello-world.postman_collection.json")
.withCopyFileToContainer(MountableFile.forClasspathResource("postman/hello-world.postman_environment.json"),
"/etc/newman/hello-world.postman_environment.json")
.withFileSystemBind("target/postman/reports", "/etc/newman/reports", BindMode.READ_WRITE)
.withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofSeconds(5)));
@Test
void run() {
NEWMAN.withCommand("run", "hello-world.postman_collection.json",
"--environment=hello-world.postman_environment.json",
"--reporters=cli,junit",
"--reporter-junit-export=reports/hello-world.newman-report.xml");
NEWMAN.start();
LOG.info(NEWMAN.getLogs());
assertThat(NEWMAN.getCurrentContainerInfo().getState().getExitCode()).isZero();
}
}