Introduction
Firstly introduced in 2023.13
/ 2023-HF23
, the HttpClientTestRule
has become the standard to test HTTP REST Endpoints.
Especially since the org.nuxeo.ecm.restapi.test.BaseTest
class has been removed in LTS 2025 because it was tied to
the Jersey Client library. As breaking changes were expected in Jersey, a complete rework of the BaseTest
class into the
HttpClientTestRule
was done.
Design
Firstly, to avoid future breaking changes as the current one, the org.nuxeo.http.test.HttpClientTestRule
was designed
to not expose the HTTP library used to perform the HTTP requests.
Secondly, this new piece of the Nuxeo Test Framework has been rewritten as a JUnit TestRule in order to correctly handled involved resources, such as HTTP connection, socket... And also to prefer the composition of its usage over extension.
Finally, the HttpClientTestRule
proposes recent APIs leveraging the Java Language functional programming. You can write
your HTTP requests to run with a builder like API, allowing to express easily mandatory and optional parameters compared
to what BaseTest
offered.
Last but not the least, you can use HttpClientTestRule
in your unit tests deploying Nuxeo bundles in an embedded servlet
container, or in your functional tests deploying Nuxeo externally as Tomcat zip or Docker image.
Maven Dependency
The org.nuxeo.http.test.HttpClientTestRule
class is available in the following dependency:
<dependencies>
..
<dependency>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-features-test</artifactId>
</dependency>
..
</dependencies>
Java Code
The HttpClientTestRule
should be declared and initialized as a JUnit test rule. The class proposes the three APIs below
to help with its creation:
defaultJsonClient(Supplier)
to configure a HTTP client with Administrator credentials and needed configuration for JSON request and response handlingdefaultClient(Supplier)
to configure a HTTP client with Administrator credentialsbuilder()
to fine configure a HTTP client
For the majority of test cases, and the help of the appropriate feature, your test would look like:
import jakarta.inject.Inject;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.nuxeo.ecm.restapi.test.RestServerFeature;
import org.nuxeo.http.test.HttpClientTestRule;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
@RunWith(FeaturesRunner.class)
@Features({ RestServerFeature.class })
public class TestMyEndpoint {
@Inject
protected RestServerFeature restServerFeature;
@Rule
public final HttpClientTestRule httpClient = HttpClientTestRule.defaultJsonClient(
() -> restServerFeature.getRestApiUrl());
...
}
The HttpClientTestRule
, with the RestServerFeature
, will be configured to hit the /nuxeo/api/v1
base URL.
This is the equivalent of extendind the BaseTest
class and using its getResponse
APIs.
Based on the piece of code above, let's see how to upgrade the REST tests.
The Nuxeo Server is initialiazed with the following documents:
/folder
aFolder
document with named folder/folder/file
aFile
document with named file and an attached blob onfile:content
/folder/picture
aPicture
document named picture and an attached picture onfile:content
Execute a GET Request
The test is about getting the file document and asserting its title.
Previously, we would have written:
import javax.ws.rs.core.Response;
import org.nuxeo.jaxrs.test.CloseableClientResponse;
@Test
public void testGet() throws Exception {
try (CloseableClientResponse response = getResponse(RequestType.GET, "/path/folder/file")) {
// Then it returns a OK
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
JsonNode node = mapper.readTree(response.getEntityInputStream());
// Then some assertion
assertEquals("file", node.get("title").asText());
}
}
Now, we write it as below:
import org.nuxeo.http.test.handler.JsonNodeHandler;
@Test
public void testGet() {
httpClient.buildGetRequest("/path/folder/file").executeAndConsume(new JsonNodeHandler(), node -> {
// HTTP status code 200 assertion has been made by JsonNodeHandler
// along with Content-Type header matching application/json
// Then some assertion
assertEquals("file", node.get("title").asText());
});
}
Execute a GET Request With Query Parameters
The test is about getting the result of CURRENT_DOC_CHILDREN
page provider with the PageProvider web adapter.
Previously, we would have written:
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.nuxeo.ecm.restapi.test.JsonNodeHelper;
import org.nuxeo.jaxrs.test.CloseableClientResponse;
import com.sun.jersey.core.util.MultivaluedMapImpl;
@Test
public void testGetWithQueryParameters() throws Exception {
MultivaluedMap<String, String> queryParameters = new MultivaluedMapImpl();
queryParameters.putSingle("sortBy", "dc:title");
queryParameters.putSingle("sortOrder", "ASC");
try (CloseableClientResponse response = getResponse(RequestType.GET, "/path/folder/@pp/CURRENT_DOC_CHILDREN",
queryParameters)) {
// Then it returns a OK
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
JsonNode node = mapper.readTree(response.getEntityInputStream());
List<JsonNode> entries = JsonNodeHelper.getEntries(node);
// Then some assertion
assertEquals(2, entries.size());
assertEquals("file", entries.get(0).get("title").asText());
assertEquals("picture", entries.get(1).get("title").asText());
}
}
Now, we write it as below:
import org.nuxeo.ecm.restapi.test.JsonNodeHelper;
import org.nuxeo.http.test.handler.JsonNodeHandler;
@Test
public void testGetWithQueryParameters() {
httpClient.buildGetRequest("/path/folder/@pp/CURRENT_DOC_CHILDREN")
.addQueryParameter("sortBy", "dc:title")
.addQueryParameter("sortOrder", "ASC")
.executeAndConsume(new JsonNodeHandler(), node -> {
// HTTP status code 200 assertion has been made by JsonNodeHandler
// along with Content-Type header matching application/json
List<JsonNode> entries = JsonNodeHelper.getEntries(node);
// Then some assertion
assertEquals(2, entries.size());
assertEquals("file", entries.getFirst().get("title").asText());
assertEquals("picture", entries.getLast().get("title").asText());
});
}
Execute a GET Request With Headers
The test is about getting the breadcrumb of the file document.
Previously, we would have written:
import javax.ws.rs.core.Response;
import org.nuxeo.ecm.restapi.test.JsonNodeHelper;
import org.nuxeo.jaxrs.test.CloseableClientResponse;
@Test
public void testGetWithHeaders() throws Exception {
try (CloseableClientResponse response = getResponse(RequestType.GET, "/path/folder/file",
Map.of("enrichers-document", "breadcrumb"))) {
// Then it returns a OK
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
JsonNode node = mapper.readTree(response.getEntityInputStream());
JsonNode contextParameters = node.get("contextParameters");
JsonNode breadcrumb = node.get("breadcrumb");
List<JsonNode> entries = JsonNodeHelper.getEntries(breadcrumb);
// Then some assertion
assertEquals(2, entries.size());
assertEquals("folder", entries.getFirst().get("title").asText());
assertEquals("file", entries.getLast().get("title").asText());
}
}
Now, we write it as below:
import org.nuxeo.ecm.restapi.test.JsonNodeHelper;
import org.nuxeo.http.test.handler.JsonNodeHandler;
@Test
public void testGetWithHeaders() {
httpClient.buildGetRequest("/path/folder/file")
.addHeader("enrichers-document", "breadcrumb")
.executeAndConsume(new JsonNodeHandler(), node -> {
// HTTP status code 200 assertion has been made by JsonNodeHandler
// along with Content-Type header matching application/json
JsonNode contextParameters = node.get("contextParameters");
JsonNode breadcrumb = node.get("breadcrumb");
// Then some assertion
List<JsonNode> breadcrumbEntries = JsonNodeHelper.getEntries(breadcrumb);
assertEquals(2, entries.size());
assertEquals("folder", entries.getFirst().get("title").asText());
assertEquals("file", entries.getLast().get("title").asText());
});
}
Execute a POST Request
The test is about creating the anotherFile document within the folder document.
Previously, we would have written:
import javax.ws.rs.core.Response;
import org.nuxeo.jaxrs.test.CloseableClientResponse;
@Test
public void testPost() throws Exception {
String uid;
try (CloseableClientResponse response = getResponse(RequestType.POST, "/path/folder/file", """
{
"entity-type": "document",
"type": "File",
"name": "anotherFile",
"properties": {
"dc:title": "anotherFile"
}
}
""")) {
// Then it returns a CREATED
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
JsonNode node = mapper.readTree(response.getEntityInputStream());
uid = node.get("uid").asText();
// Then some assertion
assertTrue(StringUtils.isNotBlank(uid));
assertEquals("anotherFile", node.get("title").asText());
}
// Then assert we can get it
try (CloseableClientResponse respone = getResponse(RequestType.GET, "/id/" + uid)) {
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
}
}
Now, we write it as below:
import static jakarta.servlet.http.HttpServletResponse.SC_CREATED;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import org.nuxeo.http.test.handler.HttpStatusCodeHandler;
import org.nuxeo.http.test.handler.JsonNodeHandler;
@Test
public void testPost() {
String id = httpClient.buildPostRequest("/path/folder/file")
.entity("""
{
"entity-type": "document",
"type": "File",
"name": "anotherFile",
"properties": {
"dc:title": "anotherFile"
}
}
""")
.executeAndThen(new JsonNodeHandler(SC_CREATED), node -> {
// HTTP status code 201 assertion has been made by JsonNodeHandler
// along with Content-Type header matching application/json
String uid = node.get("uid").asText();
// Then some assertion
assertTrue(StringUtils.isNotBlank(uid));
assertEquals("anotherFile", node.get("title").asText());
return uid;
});
// Then assert we can get it
int status = httpClient.buildGetRequest("/id/" + id).execute(new HttpStatusCodeHandler());
assertEquals(SC_OK, status);
}
Execute a PUT With Multi Part Request
The test is about updating the file:content
blob of file document.
Previously, we would have written:
import javax.ws.rs.core.Response;
import org.nuxeo.jaxrs.test.CloseableClientResponse;
import com.sun.jersey.multipart.BodyPart;
import com.sun.jersey.multipart.FormDataMultiPart;
import com.sun.jersey.multipart.file.StreamDataBodyPart;
@Test
public void testPutWithMultiPart() throws Exception {
try (FormDataMultiPart form = new FormDataMultiPart()) {
BodyPart fdp = new StreamDataBodyPart("content", new ByteArrayInputStream("modifiedData".getBytes()));
form.bodyPart(fdp);
form.field("versioning", "MINOR");
try (CloseableClientResponse response = getResponse(RequestType.PUT,
"path/folder/file/@blob/file:content", form)) {
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
}
}
}
Now, we write it as below:
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.nuxeo.http.test.handler.HttpStatusCodeHandler;
@Test
public void testPutWithMultiPart() {
var entity = MultipartEntityBuilder.create()
.addBinaryBody("content", "modifiedData".getBytes(), ContentType.TEXT_PLAIN,
"content.txt")
.addTextBody("versioning", "MINOR")
.build();
try (InputStream requestBody = entity.getContent()) {
httpClient.buildPutRequest("/path/folder/file/@blob/file:content")
.contentType(entity.getContentType().getValue())
.entity(requestBody)
.executeAndConsume(new HttpStatusCodeHandler(), status -> assertEquals(SC_OK, status.intValue()));
}
httpClient.buildGetRequest("/path/folder/file/@blob/file:content").executeAndConsume(response -> {
assertEquals(SC_OK, response.getStatus());
assertEquals("modifiedData", response.getEntityString());
});
}
Execute a DELETE Request
The test is about deleting the file:content
blob of file document.
Previously, we would have written:
import javax.ws.rs.core.Response;
import org.nuxeo.jaxrs.test.CloseableClientResponse;
import com.sun.jersey.multipart.BodyPart;
import com.sun.jersey.multipart.FormDataMultiPart;
import com.sun.jersey.multipart.file.StreamDataBodyPart;
@Test
public void testDelete() throws Exception {
try (CloseableClientResponse response = getResponse(RequestType.DELETE,
"path/folder/file/@blob/file:content")) {
}
try (CloseableClientResponse response = getResponse(RequestType.GET,
"path/folder/file/@blob/file:content")) {
assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
}
}
Now, we write it as below:
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import org.nuxeo.http.test.handler.VoidHandler;
@Test
public void testDelete() {
httpClient.buildDeleteRequest("/path/folder/file/@blob/file:content").execute(new VoidHandler())
httpClient.buildGetRequest("/path/folder/file/@blob/file:content").executeAndConsume(response -> {
assertEquals(SC_NOT_FOUND, response.getStatus());
});
}