테스트
하니스는 별도의 모듈이므로, 프로덕션 jar에는 테스트 코드가 실리지 않는다:
testImplementation 'net.benelog.spidersilk:spider-silk-test:1.1.0'
`WebTest`로 하는 종단 간 테스트
`WebTest`는 빈 포트에서 앱을 시작하고, 쿠키를 유지하는 클라이언트를 건네주고, 본문이 예외를 던지는 경우를 포함해 앱을 다시 멈춘다:
@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`와 `postForm, `postJson`은 모두 원시 `HttpResponse<String>`을 반환하므로, 단언은 프로젝트가 이미 쓰는 라이브러리에 그대로 머문다.
그 밖의 모든 것에는 `send(builder → …)`가 출구이며, `send(builder → …, bodyHandler)`는 본문을 다른 방식으로 읽는다.
바이트로 읽는 것은 압축된 응답에 필요한데, 텍스트로 디코딩하면 망가지기 때문이다.
`TestRequest`로 하는 핸들러 단독 테스트
`TestRequest`는 인자를 만들고 메서드는 직접 호출하며, 이는 핸들러에 도달하는 라우팅이 아니라 핸들러 자체를 테스트하기 위한 것이다:
@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");
}
포트도, 서블릿 컨테이너도, 목 라이브러리도 없다: queryParam, formParam, pathParam, header, cookie, body/jsonBody, file, secure(), remoteAddress(addr), sessionAttr`이 요청이 담는 것을 서술하고, `build()`가 `WebRequest`를 돌려준다.
`TestRequest.sessionAttr(key, value)`는 핸들러가 실행되기 전에 속성을 미리 두고, 핸들러 자신은 `req.setSessionAttr(key, value)`로 속성을 쓴다.
`req.host()`와 `req.scheme()`이 보고하는 호스트는 `Host 헤더에서 오므로, 테스트는 header("Host", "shop.example.com:8080")`으로 이를 서술한다.
`jsonBody`는 `Json.JsonValue 트리, 애플리케이션이 이미 선언한 JsonWriter`를 가진 값, 또는 원시 텍스트를 받는다.
원시 텍스트는 어떤 라이터도 만들어 내지 않을 본문, 예컨대 400 테스트가 보내는 잘못된 본문을 위한 형태다.
경로 변수는 매칭되는 것이 아니라 공급되는데, 라우트가 관여하지 않기 때문이다: `pathParam("deckId", "3")`은 라우터가 해석했을 값이다.
핸들러가 구별할 수 있는 모든 것은 여전히 유지된다: 같은 이름의 쿼리 파라미터와 폼 필드는 따로 남고, 헤더 조회는 대소문자를 무시하며, 업로드가 없는 요청에서 `req.file(…)`은 멀티파트가 아닌 요청과 같은 방식으로 400으로 응답한다.
본문은 컨테이너 뒤에서와 같은 방식으로 읽힌다: 두 번째 `req.body()`는 첫 번째가 보관한 텍스트로 응답하고, 그 뒤의 `req.bodyStream()`은 `IllegalStateException`을 던진다.
스텁은 리더 하나를 돌려주며 두 번째 호출에서는 이미 끝에 있으므로, `raw()`를 통해 본문을 두 번 읽는 핸들러는 프로덕션에서만 실패하는 대신 단위 테스트에서 실패한다.
`formParam 필드도 본문이며, `application/x-www-form-urlencoded`로 URL 인코딩되므로, 폼은 `body`를 함께 받을 수 없다.
파라미터 읽기는 컨테이너의 폼 파싱이 그러하듯 폼 본문을 소비하므로, 그 뒤의 `req.bodyStream()`은 비어 있고, 폼 본문을 먼저 바이트로 읽으면 필드가 남지 않는다.
같은 이름으로 `file`을 두 번 호출하면 여러 파일을 담은 필드가 만들어지며 `req.files(name)`이 이를 되읽고, 빈 파일 이름은 브라우저가 아무것도 고르지 않은 채 보낸 입력이며 `req.fileOrNull(name)`은 이에 null로 응답한다.