In case you ever developed a Jakarta Web application, you might wonder how to test JAX-RS applications. Luckily, while not perfectly documented, testing a JAX-RS application can be done in a few ways. This article covers system tests, which are somewhere between unit tests and integration tests.
Definition of system tests
To define the category of a system test, let‘s first look at what we already know about exist tests mechanisms.
- Unit tests
For unit tests, we instantiate the class under test ourselves. All required dependencies (e.g. services, usually injected via CDI) are created manually using mocks and set via a
set()
method. The response can be twofold. In case you return a POJO, you will just receive that exactly same POJO from your method call. The same is true for aResponse
object. No conversion of the entity within will be done. You will still need aResponse
implementation in your class path.- Integration tests
Class integration tests start a container and deploy the real application before the test suites are being executed. Some prefer arquillian to set up your container, while others will just start the container using various maven plugins like the
maven-failsafe-plugin
and maybe plugins provided by your favourite application server. For me, this would be theio.openliberty.tools:liberty-maven-plugin
which handles downloading the server binaries, the needed features, deploying the application and starting and stopping the application server. Also, it is somewhat more difficult to set up coverage.- System tests
Now, there are system tests, too. As stated before, they are somewhere in between unit and integration tests. They do start a JAX-RS implementation instance, but they do not use other parts of a full blown JakartaEE container. There is no CDI (which is similar to unit testing), but there is a transport (either via http, or in-memory) and all instances are initialized via the meachnisms of JakartaEE. Another benefit of these tests are that they can run in a unit-test context where coverage is easy to set up.
When to use system tests
Now that you have a bird‘s-eye view of system tests, let us look where they excel.
System tests are fast.
They are (almost) as fast as unit tests if done right. Or in pther words: they are cheap to execute. You can create lots of them without adding too much time to your build, compared to integration tests.System tests have meaning.
While unit tests do not use implementations ofMessageBodyReader
orMessageBodyWriter
, system tests do. They also use parameter converters, because you send strings via a transport.
That said, you should use system tests to test all of your endpoints. Leave unit tests only to a few logic methods in your JAX-RS endpoint — you should not have too many of those methods in your endpoints anyway. To test wheter your injection works, the application runs on your app server, use a few integration tests. For everything else, system tests are great!
Setting up a maven project with system tests
First thing you need is a maven project (obviously) with a few application modules. One module should be your war
module you are testing, contianing an Application
class and the endpoints.
For my example, I created only a few additional modules:
services/api
— where the interfaces for service classes are defined,services/impl
— where the service implementation delegates to some backend (JPA, mongoDB, whatever). We do not use such modules in system tests, usually. But you can, if the implementation is lightweight enough!commons/value
— common value classes, e.g. ID classes or parameters which can be used throughout the entire application.web/rest-v1
— (war) the actual JAX-RS module.web/api
— extracted interfaces of the JAX-RS application.
You may wonder why the interfaces for the modules are needed. Well, one of the JAX-RS implementations can use the interfaces to create a client. This will not part of THIS tutorial, but may be covered in consecutive tutorials. A big drawback: You need to repeat all of the annotations everywhere.
For the rest of this tutorial, I will use my sample project as a reference: https://github.com/bmarwell/jaxrs-testing-showcase
Choosing your implementations
While my showcase project will test multiple combinations of JAX-RS and JSONB-implementations, this is usually not needed. They are all mature and compatible with each other. However, my project is thought of a sample for most combinations, so chose your poison! :)
JAX-RS implementations
There are three main implementations:
- Eclipse Jersey
Provided by the Eclipse Foundation, this is the reference implementation. It is used widely in many application servers.
- Apache CXF
Best known for their JAX-WS support (xml+soap!), they also added a mature JAX-RS component. It was the main implementation of Open Liberty. Most other Apache application servers use Apache CXF, e.g. Apache TomEE. However, as no JakartaEE support was released recently, some application servers switched to other implementations.
- JBoss RestEasy
JBoss‘ take on JAX-RS. It features helpful additions like form/multipart capable endpoints.
JSON-B implementations
JSON-B (JSON-Binding) is the default component when (de)serializing JSON for JAX-RS. Implementations include:
- Eclipse Yasson
Again, Eclipse provides the default implementation. It is used by many application servers.
- Apache Johnzon
Also a very mature implementation, it is featured by Apache TomEE.
Creating the tests
You can now chose whether to include tests in your JAX-RS module or to make a seperate module. Usually, there is not gain in creating a separate module except you can use multiple modules for multiple implementations. Since this is rarely the case, you can just stick to having no additional module.
Another benefit might be the fact that your other tests should not inherit the classpath of the JAX-RS implementation. In this case, multiple modules can be useful.
Another benefit of more modules: Their execution are easier to control via profiles.
Another thing to chose is whether to name them *Test
or *IT
. This will have impact on coverage (depending on your coverage configuration) and by which plugin the tests are executed. I usually just go for *Test
as there is little drawback. If you need more fine-grained control, you can use JUnit @Tag()
(5/Jupiter) or @Category()
(Junit 4).`
Choosing the JSON-B implementation
For the JSON configuration, there is not much to it. Just drop the desired implementaiton dependency into your class path, e. g. one of the following dependencies:
<project>
<properties>
<dependency.johnzon.version>1.2.19</dependency.johnzon.version>
<dependency.yasson.version>3.0.2</dependency.yasson.version>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.json.bind</groupId>
<artifactId>jakarta.json.bind-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<scope>test</scope>
</dependency>
<!-- Apache Johnzon -->
<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-core</artifactId>
<classifier>jakarta</classifier>
<version>${dependency.johnzon.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-jsonb</artifactId>
<classifier>jakarta</classifier>
<version>${dependency.johnzon.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-mapper</artifactId>
<classifier>jakarta</classifier>
<version>${dependency.johnzon.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Eclipse Yasson -->
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>${dependency.yasson.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
A few additional comments here:
The johnzon dependency list is very long. This is due to the fact that they use classifiers for the jakarta namespace support, but the transitive dependencies do not. Hence, we need to exclude those and add them in later.
The scope is set to set for the JSON-Bind API. The correct scope would be
provided
for the web project andtest
for the tests.
Choosing the JAX-RS implementation
Now we need to include any of the JAX-RS implementations in our module. Here is the relevant for your pom.xml:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-extension-providers</artifactId>
<version>${dependency.cxf.version}</version>
<scope>test</scope>
</dependency>
or
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>3.1.0-M8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>3.1.0-M8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>3.0.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>3.1.0-M8</version>
<scope>test</scope>
</dependency>
or
<project>
<properties>
<version.org.jboss.resteasy>6.2.1.Final</version.org.jboss.resteasy>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-bom</artifactId>
<version>${version.org.jboss.resteasy}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-undertow-cdi</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jboss.jandex</groupId>
<artifactId>jandex-maven-plugin</artifactId>
<version>1.2.3</version>
<executions>
<execution>
<id>make-index</id>
<goals>
<goal>jandex</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
While those are the dependencies you need, I haven‘t got my example working. The dependencies are just shown for completeness.
Adding a MessageBodyWriter
Usually, this is done for you by your Application server. However, in this case you will need to create your own MessageBodyWriter.
Instead of typing everything here, just copy parts of this module: JAX-RS JSONB support. This folder contains both a MessageBodyWriter and a JSONB context class.
Implementing tests
Now that everything is set up, we can finally start writing tests! I need to add: Writing tests for Jersey is both simpler and more straight forward. Their dependency has everything included you will need, and you can use the default JaxRS Client
class. For Apache CXF, you will want to write your own Junit Jupiter extension (no worries, I got you covered!) and you will be using their client whenever you chose local transport.
Eclipse Jersey: Just start writing tests
With Eclipse Jersey, it is really easy to start tests. Once you added the correct dependency, it is just a matter of writing your tests.
Everything you need to do is to extend the JerseyTest
class, which is already in your class path. You will only need two additional methods, one of them encapsulated in a nested class like so:
public class AuthorResourceTest extends JerseyTest {
private static final JsonbContext jsonbContext = new JsonbContext();
public static class ApplicationBinder extends AbstractBinder {
@Override
protected void configure() {
bind(queryService).to(BookstoreQueryService.class).ranked(1);
bind(AuthorMapper.class).to(AuthorMapper.class).ranked(1);
bind(jsonbContext.getJsonb()).to(Jsonb.class);
}
}
@Override
protected Application configure() {
// 0 == find first available port.
forceSet(TestProperties.CONTAINER_PORT, "0");
return new ResourceConfig(AuthorResourceImpl.class)
.register(new ApplicationBinder())
.register(new JsonbJsonMessageBodyWriter());
}
}
The inner static class ApplicationBinder
is used to configure injection - similar to what Google Guice does. In the configure()
method, which is overridden from JerseyTest
, you can then use an instance of this class to make those bindings known to Jersey. Please note I also register the MessageBodyWriter for JSON using our Jsonb implementation.
Now an actual test can be written in a similar way to what you do in unit tests:
public class AuthorResourceTest extends JerseyTest {
private static final BookstoreQueryService queryService = mock(BookstoreQueryService.class);
@Test
public void author_by_id_returns_author() {
// given
when(queryService.queryAuthors(any(AuthorQuery.class)))
.then(args -> makeAnswerFromQuery(args.getArgument(0)));
// when
final Response authorByIdResponse = target("authors/rpfeynman")
.request(MediaType.APPLICATION_JSON_TYPE)
.get(Response.class);
// then
assertThat(authorByIdResponse)
.hasFieldOrPropertyWithValue("status", 200)
.extracting(rsp -> rsp.readEntity(String.class))
.extracting(jsonStr -> jsonbContext.getJsonb().fromJson(jsonStr, Map.class))
.hasFieldOrPropertyWithValue("id", "rpfeynman");
}
}
I omitted the method makeAnswerFromQuery()
, because it only sets up the mocked Service to return a useful result.
The test is using the standard JAX-RS Client API, which is good — this is what Java clients should be using in production. The assertion will check that the response completed successfully, then extracting the response and converting it back to a Java object.
You can find the complete example class on GitHub.
Apache CXF: Writing a Junit-Jupiter extension
First thing we need is an extension to make Apache CXF available to Junit. The extension will manage the CXF server lifecycle as well as provide some methods to get an instance of the Apache CXF Client implementation. The extension is written in a way that you can provide either instances or just the class - the extension will figure out how to instantiate resource classes under test. Please note for simplicity, I only implemented BeforeAllCallback, AfterAllCallback
— thus the server will start before ALL the tests and be shut down after ALL the tests were executed.
The extension will also register prodiers, features and resource classes and provides .with*()
methods for setting up those, which makes it very easy to use — maybe even a bit easier than Eclipse Jersey. As always, you can find the source code on GitHub.
Now with the extension set up, we can easily write our tests.
public class AuthorResourceTest {
private static final AuthorResourceImpl authorResource = new AuthorResourceImpl();
private static final BookstoreQueryService queryService = mock(BookstoreQueryService.class);
private static final AuthorMapper mapper = new AuthorMapper();
private static final JsonbContext jsonbContext = new JsonbContext();
@RegisterExtension
static CxfLocalTransportExtension ltp = new CxfLocalTransportExtension()
.withResource(AuthorResourceImpl.class, authorResource)
.withProvider(new JsrJsonbProvider(jsonbContext.getJsonb()));
@BeforeAll
public static void setUpResource() {
authorResource.setQueryService(queryService);
authorResource.setAuthorMapper(mapper);
}
@AfterAll
public static void closeJsonbContext() throws Exception {
jsonbContext.close();
}
}
As you can see, the set up is quite similar except that we use Junit-Jupiter‘s `@RegisterExtensionOther then that, ` annotation.
Now that this is set up, we can start writing tests the same way — except that we need to use the Apache CXF client implementation:
public class AuthorResourceTest {
@Test
public void author_by_id_returns_author() {
// given
when(queryService.queryAuthors(any(AuthorQuery.class)))
.then(args -> makeAnswerFromQuery(args.getArgument(0)));
// when
final Response authorByIdResponse = ltp.getWebClient()
.accept("application/json")
.path("authors/rpfeynman")
.get();
// then
assertThat(authorByIdResponse)
.hasFieldOrPropertyWithValue("status", 200)
.extracting(rsp -> rsp.readEntity(String.class))
.extracting(jsonStr -> jsonbContext.getJsonb().fromJson(jsonStr, Map.class))
.hasFieldOrPropertyWithValue("id", "rpfeynman");
}
}
As you can see, the test class looks almost identical, except that we do not use the JAX-RS client directly.
JBoss RestEasy
JBoss RestEasy can be tested using the resteasy-undertow-cdi
dependency. Sadly, I was unable to get it running with CDI injection as of now. Therefore, JBoss RestEasy system tests will be covered in a later article
Outlook
There is much more to testing JAX-RS applications than this guide could cover in a single article. Later blog posts will cover:
Integration testing using various application servers, e. g. Open Liberty.
JBoss RestEasy system tests.
Apache Shiro integration.
… you tell me! :)
References
Full example project ib GitHub:
github.com/bmarwell/jaxrs-test/showcaseEclipse Jersey: Test Framework:
https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/test-framework.htmlApache CXF: JAX-RS Testing:
https://cwiki.apache.org/confluence/display/CXF20DOC/JAXRS+TestingApache CXF: Local Transport:
https://cxf.apache.org/docs/local-transport.htmlRestEasy: bootstrap-cdi example:
https://github.com/resteasy/resteasy-quickstarts/pull/2/files