Skip to main content

Test an endpoint with MockRestClient

What you'll build​

A small greeting resource and a JUnit test class that calls it through MockRestClient. The mock client sends real requests and parses real responses, but hands them straight to the resource in the same JVM, so no server starts and no port is opened. The three tests check a JSON response, a request body round-trip and a 400 error, and mvn test reports them passing.

Prerequisites and time​

  • Java 17 or newer and Maven 3.9 or newer.
  • org.apache.juneau:juneau-rest-server:10.0.0-SNAPSHOT for the resource.
  • org.apache.juneau:juneau-rest-mock:10.0.0-SNAPSHOT (test scope), which brings the REST client with it.
  • org.junit.jupiter:junit-jupiter:6.0.3 (test scope).
  • About 10 minutes.

10.0.0-SNAPSHOT needs the Apache snapshots repository, which the pom.xml below declares; Maven Central has only the 9.2.0 release today (see Install Juneau and Downloads).

Steps​

1. Create the project​

Make an empty directory and save this as pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>greetings</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<juneau.version>10.0.0-SNAPSHOT</juneau.version>
</properties>

<repositories>
<repository>
<id>apache-snapshots</id>
<url>https://repository.apache.org/snapshots/</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server</artifactId>
<version>${juneau.version}</version>
</dependency>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-mock</artifactId>
<version>${juneau.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
</plugins>
</build>
</project>

The two main classes go in src/main/java/com/example/greetings/ and the test in src/test/java/com/example/greetings/. Start each file with package com.example.greetings;.

2. Write the bean​

Greeting.java:

/**
* The bean the greeting service returns.
*/
public class Greeting {

/** Who is greeted. */
public String name;

/** The greeting text. */
public String message;

/** Used by the parser. */
public Greeting() {}

/**
* Creates a greeting for a name.
*
* @param name Who is greeted.
*/
public Greeting(String name) {
this.name = name;
this.message = "Hello, " + name + "!";
}
}

3. Write the resource​

GreetingResource.java has one GET and one POST. The POST rejects a blank name with a 400:

import org.apache.juneau.http.*;
import org.apache.juneau.http.response.*;
import org.apache.juneau.rest.server.*;
import org.apache.juneau.rest.server.servlet.*;

/**
* A small greeting service: one GET, one POST that validates its body.
*/
@Rest(path = "/greetings", title = "Greeting service")
public class GreetingResource extends BasicRestServlet {

private static final long serialVersionUID = 1L;

/**
* Greets the name in the path.
*
* @param name The name from the URL.
* @return The greeting.
*/
@RestGet("/{name}")
public Greeting greet(@Path("name") String name) {
return new Greeting(name);
}

/**
* Greets the name in the request body.
*
* @param body A greeting with only {@code name} set.
* @return The completed greeting.
* @throws BadRequest If the name is missing or blank.
*/
@RestPost("/")
public Greeting create(@Content Greeting body) throws BadRequest {
if (body.name == null || body.name.isBlank())
throw new BadRequest("name is required");
return new Greeting(body.name);
}
}

4. Write the test​

GreetingResource_Test.java:

import static org.junit.jupiter.api.Assertions.*;

import org.apache.juneau.marshall.json.*;
import org.apache.juneau.rest.mock.*;
import org.junit.jupiter.api.*;

class GreetingResource_Test {

@Test
void a01_getReturnsJson() throws Exception {
try (var mock = MockRestClient.create(GreetingResource.class);
var resp = mock.get("/Ada").header("Accept", "application/json").run()) {
resp.assertThat().isOk().body().contains("\"message\":\"Hello, Ada!\"");
}
}

@Test
void a02_postParsesBodyIntoBean() throws Exception {
try (var mock = MockRestClient.builder(GreetingResource.class).defaultParser(JsonParser.DEFAULT).build();
var resp = mock.post("/").body(new Greeting("Grace")).run()) {
resp.assertOk();
var greeting = resp.body().as(Greeting.class);
assertEquals("Hello, Grace!", greeting.message);
}
}

@Test
void a03_blankNameIsRejected() throws Exception {
try (var mock = MockRestClient.builder(GreetingResource.class).noTrace().build();
var resp = mock.post("/").bodyString("{\"name\":\"\"}").header("Content-Type", "application/json").run()) {
resp.assertStatus(400);
}
}
}

Run it​

mvn test

The end of the output:

[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] BUILD SUCCESS

Each request also leaves a one-line INFO access record in the log, with its status, method and path.

How it works​

  • No server. MockRestClient.create(GreetingResource.class) builds the resource's RestContext and gives you a RestClient whose transport calls it directly. The context is built once per resource class and reused, so creating a mock client in every test is cheap. See NG Mock REST Client.
  • Paths are relative to the resource. The mock client's root URL already ends in the resource's @Rest(path), so mock.get("/Ada") reaches /greetings/Ada.
  • Ask for the format you test. With no Accept header the resource answers with its HTML view, so the first test sends Accept: application/json. To read a response as a bean with body().as(...), give the builder a parser with defaultParser(...), as the second test does.
  • Expected errors stay quiet. noTrace() sends No-Trace: true, so the 400 the third test expects does not put a stack trace in the log.
  • Two flavors. This guide uses org.apache.juneau.rest.mock.MockRestClient, which pairs with the next-generation REST client. Tests written against the classic client use org.apache.juneau.rest.mock.classic.MockRestClient, described in Mock REST Client Overview. Both ship in juneau-rest-mock.

Next steps​