As the name of the project suggests, this aims to be a demonstration on how to create a simple application which connects to an external REST service and how to write integration tests (acceptance tests) against the program using cucumber for testing and wiremock for mocking the external service.
The example project implements an application mvn-query
which has a single command search
. The application queries Maven packages from the Maven central server.
E.g. mvn-query search guice -h=100
would return the first 100 artifacts whose ID is matching the guice
keyword.
The app uses apache HTTP to connect to the maven server (currently https://search.maven.org) and queries the packages via the REST API (https://search.maven.org/classic/#api).
The structure of the project is the following:
- /cucumber-wiremock-example <- root project
- /app <- implementation of the
mvn-query
application - /buildSrc <- definition of common gradle conventions (common dependencies etc.)
- /integration-test <- cucumber tests for the
app
project - /lib <- library to connect to the MVN server
- /app <- implementation of the
Running the application:
cd $projectRoot/app
gradle run --args="search gui"
- java 11+
- gradle
- cucumber - testing framework
- gherkin - language of cucumber tests
- wiremock - simulator for REST service
- micronaut - full-stack framework
- fasterxml/jackson - java JSON library
- lombok - boiler-plate code generation
- picocli - command line interface
Cucumber is a testing framework which allows users to define test cases in plain text, using simple plain english (or french, or german ect. but human readable). Test cases are defined using gherkin, which is the syntax of the cucumber tests, a set of rules and keywords which gives a structure text so it becomes an executable specification for the test framework. Cucumber has binding to several programming languages (amongst them java).
Essentially a test case looks like the following:
Feature: Ping command
The app can ping a website
Background:
Given an installation of the application
And a server is available at "www.google.com"
Scenario: Ping a website and the server responds
When I run "ping www.google.com"
Then the command returns with success
And the following lines are included in the standard output
| PING www.google.com (${ip_matcher}) ${byte_count} bytes of data. |
The test case above tests the ping command. It has the following parts:
- Feature: A collection of test cases (scenarios) which belongs to a unit of functionality (feature) in the application under test
- Background: Preconditions (similar to JUnit
@BeforeEach
). The things set here will be the boundary condition for all of scenarios in this feature. - Scenario: A test case
- Given, When, Then, And and But: Steps in the test which are executed sequentially
- Given: some precondition
- When: an event which happens to the application under test (e.g. user input, trigger)
- Then: an assertion where we validate the state of our application
- And, But: can replace any of the keywords above to bundle multiple statements for better readability
Underneath we can implement the programmatic part of the instructions like:
@When("^I run {string}$")
public void whenIRunCommand(String command) {
testContext.setReturnCode(new AppUnderTest().execute(command.split(" ")))
}
The framework will substitute the whenIRunCommand
method when the regular expression in the @When
annotation matches the instruction in the gherkin file. The method will be invoked with a parameter, which is extracted by the {string}
matcher.
Some pros for using cucumber:
- supports behavior-driven development: it is up to you whether you see BDD as a pro or con
- programming language independent: you can easily re-implement a micro-service or two in a brand new language keeping the tests intact
- human readable: I hate writing requirement specification, also integration tests are not my favorite thing, so why not kill two birds with one stone? Properly written gherkins can be used as requirement specs as non-technical stakeholders can also understand it
- enforces the same structure for tests: gherkins are well structured, it is easy to oversee deviations during code-reviews
Some cons:
- setting up in a project needs some work: small applications might not worth it
- can be hard to oversee: you need to jump back-and-forth between the gherkin and the underlying code to understand
- can be expensive to maintain: especially for complex systems
- tests might not be independent form each other: if you try to reduce complexity by sharing functionality, backgrounds and steps among features and test cases, you will end up with the fact that changing a background step might affect several features.
Wiremock is a simulator for HTTP-based APIs or simply a mock server. Essentially it listens on a given address and port and answers HTTP requests as configured.
e.g. I can configure the wiremock to respond any HTTP POST request to return a status 200 (OK) with an empty body, to mock the upload in my application.
Wiremock can be a standalone process on a remote server, or on the same host as the tested system or can be a library used by a JVM application. We will concentrate on the latter.
Micronaut is a framework for building micro-service applications in Java, Kotlin or groovy. It uses DI containers which perform dependency injection at compile time, which reduces memory footprint and startup time. Basically you have all the functionality which you need for a desktop application or standalone server application without referencing the whole Spring framework.
I especially like to use Micronaut in combination with picocli and graalVM, because I can create a command line application which is small and fast yet it has the same flexibility as any micro-service compiled into a single JAR.
Moreover picocli commands and subcommands implemented as a micronaut @Singleton
has the exact same structure as REST API commands.
I"m going to try to give a step-wise guide how to set up a project like this. I assume that you are familiar with gradle projects, if not there are plenty of guides on the web.
Note: I prefer gradle over maven, because I find it faster, more flexible and better readable but it is up to you to decide of course.
Micronaut has a CLI where you can initialize new micronaut applications from templates. Also micronaut has a gradle plugin, which configures the micronaut BOM and sets up annotation processing. I"m gonna skip that as I find it more educational to do it by hand and understand how the setup works.
annotationProcessor "io.micronaut:micronaut-inject-java:3.8.7"
implementation "io.micronaut:micronaut-inject:3.8.7"
testAnnotationProcessor "io.micronaut:micronaut-inject-java:3.8.7"
Micronaut beans are configured with jakarta.inject
annotations implemented in the micronaut library, so we need micronaut to do the annotation processing and we need the implementation to access the API.
The ApplicationContext is the main entry point for starting and running Micronaut applications. It can be thought of as a container object for all dependency injected objects. The ApplicationContext can be started via the run()
method. Or alternatively, the builder()
method can be used to customize the ApplicationContext using the ApplicationContextBuilder interface prior to running. I prefer the latter as it gives more control.
We need to create this single entry point in our application:
public static void main(String... args) {
try (var context = ApplicationContext
.builder(Main.class)
.start()) {
System.out.println("Application has been started!")
}
}
The builder()
needs the main class of the application (I guess it needs it to get the current class loader) and optionally an environment to configure the framework. The default environment is FUNCTIONAL
so I omitted the environment.
The start()
method creates the context and finalizes the configuration. We haven"t configured much for now.
The context
should be used in a try-resource or closed manually otherwise as we need to close it to invoke all the AutoCloseable
beans" close()
method and free up any resources needed (like releasing a sever connection).
Now we need some services to inject. For example here are two test services:
@Singleton
class DBConnector implements AutoClosable {
DBConnector() {
//connects to database
}
@Override
public void close() {
//releases db connection
}
}
@Singleton
class QueryService {
private final DBConnector connector;
@Inject
QueryService(DBConnector connector) {
this.connector = connector;
}
public List<String> listAllTables() {...}
}
The DBConnector
is a service to connect to some database the first time the service is used and release the database connection when we close the application context.
QueryService
uses the DBConnector
to run some queries on the database like listing all data tables. The DBConnector
is injected into the QueryService
. Note that the @Inject
annotation could be omitted and the code would still run, I just put it here for to indicate the injection.
Now we need to run the service in the main function:
try (var context = ApplicationContext
.builder(Main.class)
.start()) {
var qService = context.findOrInstantiateBean(QueryService.class).orElseThrow(
() -> new RuntimeException("Could not instantiate QueryService bean!"));
qService.listAllTables().forEach(System.out::println);
}
The findOrInstantiateBean
, as the name suggests, finds an already existing bean or creates a new one returning an Optional<T>
which is empty()
if the instantiation was not successful.
If you want to consume a service from a third party library, which is not a micronaut bean you have two options: wrap it in a bean class (or provider etc.), register it as singleton.
Let"s assume we want to create a service from the java.util.Random()
class.
- Wrap it in a bean class This is pretty straight forward:
@Singleton
class RandomBean {
@Getter
private final Random boxed = new Random();
}
- Register is as singleton
We can register any number of singletons before issuing the
start()
method of the application context.
public static void main(String... args) {
try (var context = ApplicationContext
.builder(Main.class)
.singletons(new Random())
.start()) {
var randomService = context.findOrInstantiateBean(Random.class)
...
}
}
Picocli is a command line interface library for Java, which has basically all the features you will ever need for creating a CLI application. What"s even better is that it has a seamless integration with a number of popular java frameworks, micronaut amongst them.
MicronautPicocli defines commands and sub-commands as micronaut beans, which gets instantiated as the picocli command line parser matches the commands and arguments registered for the bean.
e.g.
@Singleton
@CommandLine.Command(name = "ping")
class PingCommand {
@CommandLine.Parameters
private String url;
}
Is instantiated when the user gives the app ping www.google.com
command.
implementation "info.picocli:picocli:4.7.1"
implementation "io.micronaut.picocli:micronaut-picocli:4.3.0"
picocli
artifact is the library itself, micronaut-picocli
is the micronaut integration with the picocli framework.
Use the QueryService
above, we want to achieve the following:
- if the user inputs
app
without parameters we will print hello - if the user inputs
app query something
we will print something
First lets annotate the main class:
@Singleton
@CommandLine.Command(name = "app")
class App implements Runnable {
public static void main(String... args) {
try (var context = ApplicationContext
.builder(Main.class)
.start()) {
//TODO we will create the command line here later
}
}
@Override
public void run() {
System.out.println("hello");
}
}
Then the service class
@Singleton
@CommandLine.Command(name = "query")
@RequiredArgsConstructor
class QueryService implements Runnable {
private final DBConnector connector;
@CommandLine.Parameters
private String param;
@Override
public void run() {
System.out.println(param)
}
}
As you can see, we implemented the Runnable
interface, as the picocli framework will execute the run()
command when the associated command is the input. Other possibility is to use a Callable<T>
interface to provide a result for the command.
We need to create and configure the picocli command line to execute when the app runs.
So we need to extend the main(...)
function.
public static void main(String... args) {
try (var context = ApplicationContext
.builder(App.class)
.start()) {
new CommandLine(this, new MicronautFactory(context)).execute(args);
}
}
The CommandLine
will match the given arguments against the annotated commands and parameters and use the MicronautFactory
to instantiate the necessary beans for the execution through the micronaut framework.
Now we will create an integration test project (I prefer this term to acceptance tests as the name, acceptance, indicates that these tests are only necessary for some higher power to accept the code changes).
Create a new sub-module and add the implementation project as dependency. In the example we will use JUnit5 so you will also need to run with jupiter:
dependencies {
...
testImplementation "org.junit.jupiter:junit-jupiter-api:$versionJunitJupiterApi"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
}
tasks.named("test") {
useJUnitPlatform()
}
testImplementation "io.cucumber:cucumber-java:6.10.2"
testImplementation "io.cucumber:cucumber-junit-platform-engine:6.10.2"
Create a new class under the src/test/java
called TestRunner
@Cucumber
class TestRunner {
}
This class is a necessary entry point for the cucumber testing. It can stay empty, but you can implement setup and tear-down functionality in it (e.g. to delete files from the filesystem)
Cucumber has its built-in dependency injection to instantiate implementation classes with its limitation. If you wish to use micronaut instead, then:
- implement the cucumber
ObjectFactory
interface to use micronaut:
public final class TestObjectFactory implements ObjectFactory {
private ApplicationContext context;
@Override
public void start() {
context = ApplicationContext.builder(Application.class, Environment.TEST).start();
}
@Override
public void stop() {
context.close();
}
/**
* @return Return true on trying to add a class always, as the re-instantiating is handled in the getInstance method
*/
@Override
public boolean addClass(final Class<?> someClass) {
return true;
}
@Override
public <T> T getInstance(final Class<T> clazz) {
final Optional<T> bean = context.findBean(clazz);
return bean.orElseGet(() -> InstantiationUtils.instantiate(clazz));
}
- create a new file:
$projectDir/src/test/resources/META-INF/services/io.cucumber.core.backend.ObjectFactory
- put the class path of the custom
ObjectFactory
into the file e.g.:phasza.java.cucumber.example.test.TestObjectFactory
Put the feature files into $projectDir/src/test/resources/phasza/java/cucumber/example/test/features/Search.feature
, where of course the part after resources
matches the package where the TestRunner
class resides, otherwise the cucumber won"t see your tests.
Write the feature tests using the gherkin syntax.
Implement the steps in steps classes, context similarly to the example project.
testImplementation "com.github.tomakehurst:wiremock:2.27.2"
An example:
@Singleton
public class MockServer implements AutoClosable {
private final WireMockServer server;
public MockServer() {
server = new WireMockServer(options().dynamicPort());
}
@Override
public void close() {
server.stop()
}
}
which would fire up a new wiremock server on a dynamically allocated port. For more info about configuration see the wiremock documentation or the example project
The variation of stubbing is huge, so check out the example project and the wiremock documentation.