Testing
The harness is its own module, so the production jar carries no test code:
testImplementation 'net.benelog.spidersilk:spider-silk-test:1.1.0'
End to end, with WebTest
WebTest starts the app on a free port, hands you a client that keeps cookies, and stops it again, including when the body throws:
@Test
void createsADeck() {
WebTest.test(app, client -> {
var created = client.postForm("/decks", Map.of("name", "English"));
assertThat(created.statusCode()).isEqualTo(302); // redirects are not followed
assertThat(client.get("/api/decks").body()).contains("English");
});
}
get/post/put/patch/delete/head/options, plus postForm and postJson, all return the raw HttpResponse<String>, so assertions stay in whatever library the project already uses.
send(builder → …) is the way out for anything else, and send(builder → …, bodyHandler) reads the body some other way.
Reading it as bytes is what a compressed answer needs, since decoding it as text would destroy it.
The handler alone, with TestRequest
TestRequest builds the argument and you call the method, for a test of the handler itself rather than of the routing that reaches it:
@Test
void createDeckRespondsWith201() {
Json.JsonObject requestBody = Json.obj().put("name", "Spanish");
WebResponse response = controller.createDeck(TestRequest.post("/api/decks")
.jsonBody(requestBody)
.build());
assertThat(response.status()).isEqualTo(HttpStatus.CREATED);
assertThat(deckService.getDeck(idFrom(response)).name()).isEqualTo("Spanish");
}
No port, no servlet container, and no mock library: queryParam, formParam, pathParam, header, cookie, body/jsonBody, file, secure(), remoteAddress(addr), and sessionAttr state what the request carries, and build() hands back a WebRequest.
TestRequest.sessionAttr(key, value) puts an attribute in place before the handler runs, and the handler itself writes one with req.setSessionAttr(key, value).
The host that req.host() and req.scheme() report comes from the Host header, so header("Host", "shop.example.com:8080") is how a test states it.
jsonBody takes a Json.JsonValue tree, a value with the JsonWriter the application already declares, or raw text.
Raw text is the form for a body no writer would produce, such as the malformed one a 400 test sends.
Path variables are supplied rather than matched, since no route is involved: pathParam("deckId", "3") is what the router would have resolved.
Everything a handler can tell apart still holds: a query parameter and a form field of the same name stay separate, header lookup ignores case, and req.file(…) on a request with no upload answers 400 the way a non-multipart request does.
The body is read the way it is behind a container: a second req.body() answers the text the first one kept, and req.bodyStream() after it throws IllegalStateException.
The stub hands back one reader, already at its end on the second call, so a handler that reads the body twice through raw() fails the unit test rather than only failing in production.
formParam fields are the body as well, URL-encoded under application/x-www-form-urlencoded, so a form cannot also take body.
A parameter read spends a form body as a container’s form parsing does, so req.bodyStream() after it is empty, and a form body read as bytes first leaves no fields.
file called twice with one name builds the field that carries several files, which req.files(name) reads back, and an empty file name is the input a browser sent with nothing chosen, which req.fileOrNull(name) answers null for.