Jettyに、LocalConnectorというConnectorがあることを知りまして。
LocalConnector (Jetty :: Project 9.4.8.v20171121 API)
こちらはテスト目的のConnectorで、こんな感じに使うそうな。
HttpTester.Request request = HttpTester.newRequest();
request.setURI("/some/resource");
HttpTester.Response response =
HttpTester.parseResponse(HttpTester.from(localConnector.getResponse(request.generate())));
これ以上、ドキュメントに載っていない気も…。
LocalConnector?
LocalConnectorは、どうやらこういうもののようです。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java
なので、単体テストあたりで使う目的のものですね。簡単にJettyに載せたアプリケーションを確認できそうな感じです。
まあ、使ったイメージを見た方が早いと思うので、サンプルを書いていってみましょう。
とりあえず
Jetty上で動かすServletを作りましょう。
まずは、Maven依存関係から。
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> <version>9.4.8.v20171121</version> </dependency>
最小構成のServletでいきます。
こんな感じで。
src/test/java/org/littlewings/embedded/jetty/TestServlet.java
package org.littlewings.embedded.jetty; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Optional; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class TestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { execute(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { execute(request, response); } void execute(HttpServletRequest request, HttpServletResponse response) throws IOException { response .getWriter() .write("Hello " + Optional.ofNullable(request.getParameter("name")).orElse("Servlet") + "!!"); } }
テストコードで確認するので、テスト系のライブラリも足しておきます。
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.0.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.9.0</version> <scope>test</scope> </dependency>
LocalConnector+HttpTester
では、LocalConnectorを使って、作成したServletでデプロイしてテストしてみます。
使用するのはLocalConnectorとHttpTester。HttpTesterを使うには、次の依存関係が必要です。
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-http</artifactId> <classifier>tests</classifier> <scope>test</scope> <version>9.4.8.v20171121</version> </dependency>
classifierが「tests」の「jetty-http」です。こちらに、HttpTesterが含まれています。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java
なお、LocalConnectorについては、「jetty-server」に含まれており、こちらは「jetty-servlet」などを依存関係に入れると
一緒に引き込まれます。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java
作成してみたテストコードは、こちら。
src/test/java/org/littlewings/embedded/jetty/JettyLocalConnectorTest.java
package org.littlewings.embedded.jetty; import java.nio.charset.StandardCharsets; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletHandler; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class JettyLocalConnectorTest { @Test public void servletTestHttpGet() throws Exception { Server server = new Server(); LocalConnector connector = new LocalConnector(server); server.addConnector(connector); ServletHandler handler = new ServletHandler(); handler.addServletWithMapping(TestServlet.class, "/test"); server.setHandler(handler); server.start(); HttpTester.Request request = HttpTester.newRequest(); request.setHeader(HttpHeader.HOST.asString(), "localhost"); request.setURI("/test?name=Jetty"); HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(connector.getResponse(request.generate()))); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); assertThat( new String(response.getContentBytes(), StandardCharsets.UTF_8) ).isEqualTo("Hello Jetty!!"); server.stop(); } @Test public void servletTestHttpPost() throws Exception { Server server = new Server(); LocalConnector connector = new LocalConnector(server); server.addConnector(connector); ServletHandler handler = new ServletHandler(); handler.addServletWithMapping(TestServlet.class, "/test"); server.setHandler(handler); server.start(); HttpTester.Request request = HttpTester.newRequest(); request.setMethod(HttpMethod.POST.asString()); request.setHeader(HttpHeader.HOST.asString(), "localhost"); request.setHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.FORM_ENCODED.asString()); request.setURI("/test"); request.setContent("name=Jetty".getBytes(StandardCharsets.UTF_8)); HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(connector.getResponse(request.generate()))); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); assertThat( new String(response.getContentBytes(), StandardCharsets.UTF_8) ).isEqualTo("Hello Jetty!!"); server.stop(); } }
Jetty自身のテストコードを参考にして、書いてみました。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/PostServletTest.java
使い方は、最初にJettyを表すServerを作成しますが、この時にポート指定は必要ありません。このServerを使ってLocalConnectorを
作成して、ConnectorをServerに登録します。
Server server = new Server(); LocalConnector connector = new LocalConnector(server); server.addConnector(connector);
今回は簡単にServletを登録して
ServletHandler handler = new ServletHandler(); handler.addServletWithMapping(TestServlet.class, "/test"); server.setHandler(handler);
Server#start。
server.start();
Serverが起動したら、HTTPリクエストを作成します。HttpTester.Requestを作りましょう。
GETリクエストの場合は、最低限これくらいの設定が必要です。
HttpTester.Request request = HttpTester.newRequest(); request.setHeader(HttpHeader.HOST.asString(), "localhost"); request.setURI("/test?name=Jetty");
HttpTester#newRequestで作成されるHttpTester.Requestには、GET、リクエストパスが「/」、HTTP 1.1を使うこと、くらいの
設定しかないので、Hostヘッダから指定してあげる必要があります。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-http/src/test/java/org/eclipse/jetty/http/HttpTester.java#L70-L77
POSTの場合は、こんな感じ。
HttpTester.Request request = HttpTester.newRequest(); request.setMethod(HttpMethod.POST.asString()); request.setHeader(HttpHeader.HOST.asString(), "localhost"); request.setHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.FORM_ENCODED.asString()); request.setURI("/test"); request.setContent("name=Jetty".getBytes(StandardCharsets.UTF_8));
HTTPボディは、HttpTester.Request#setContentで指定します。今回はKey&Value形式ですが、Content-Typeは
「application/x-www-form-urlencoded」にしておく必要があります、と。
HttpTester.Requestの作り方は、他にもHTTPリクエストそのものを文字列やByteBuffer、InputStreamからパースして
作成することもできます。が、いずれもHTTPでリクエストする内容を直接意識するものなので、抽象度はどれを使っても
それほど変わらないかも…。
Jettyのテストでは、HTTPリクエストを文字列で組み立てています。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/PostServletTest.java#L122-L149
リクエストはLocalConnector#getResponseに渡せばOKです。これで今回のServletからのレスポンスを得られるのですが、
返ってくるのがByteBufferだったりStringだったりするので、パースする必要があります。これには、HttpTester#fromとHttpTester#parseを
使うことになります。
つまり、こうなる、と。
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(connector.getResponse(request.generate())));
LocalConnectorに直接リクエストを投げ込むんですよ、リッスン先がないからそうなののか…と。この時、HttpTester.Request#generateで、ここまで
構築したHTTPリクエストをByteBufferで渡しています。
結果の確認。
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); assertThat( new String(response.getContentBytes(), StandardCharsets.UTF_8) ).isEqualTo("Hello Jetty!!");
終わったら、Server#stop。
server.stop();
こんな感じの使い方です。
なお、ここのLocalConnector#getResponseの部分ですが
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(connector.getResponse(request.generate())));
内部的にはリクエストをQueueに入れ、それからサーバー側で処理した結果が入るか、指定時間まで待って終わらなかったら終了するようになっています。
今回のJettyのバージョン、「9.4.8.v20171121」だとなにも指定しないとサーバー側の応答を最大10秒待ちます。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java#L224
これを調整するには、LocalConnector#getResponseに明示的に最大の待ち時間を指定します。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java#L234
レスポンスがすぐ戻ってくればよいのですが、そうでない場合はしばらく待つことになるので、覚えておきましょう。
ServletTester
とまあ、こんな感じにServer+LocalConnectorを使うのですが、これをもうちょっと簡略したものにServletTesterがあります。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletTester.java
ServletTesterは、「jetty-servlet」の「tests」classifierに含まれています。
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<classifier>tests</classifier>
<scope>test</scope>
<version>9.4.8.v20171121</version>
</dependency>
中身は、ServerとLocalConnectorを内包したものになります。
まあ、気持ちコードが短くなる程度ですね。
src/test/java/org/littlewings/embedded/jetty/JettyServletTest.java
package org.littlewings.embedded.jetty; import java.nio.charset.StandardCharsets; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletTester; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class JettyServletTest { @Test public void servletTestHttpGet() throws Exception { ServletTester tester = new ServletTester(); ServletHandler handler = new ServletHandler(); handler.addServletWithMapping(TestServlet.class, "/test"); tester.getServer().setHandler(handler); tester.start(); HttpTester.Request request = HttpTester.newRequest(); request.setHeader(HttpHeader.HOST.asString(), "localhost"); request.setURI("/test?name=Jetty"); HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(tester.getResponses(request.generate()))); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); assertThat( new String(response.getContentBytes(), StandardCharsets.UTF_8) ).isEqualTo("Hello Jetty!!"); tester.stop(); } @Test public void servletTestHttpPost() throws Exception { ServletTester tester = new ServletTester(); ServletHandler handler = new ServletHandler(); handler.addServletWithMapping(TestServlet.class, "/test"); tester.getServer().setHandler(handler); tester.start(); HttpTester.Request request = HttpTester.newRequest(); request.setMethod(HttpMethod.POST.asString()); request.setHeader(HttpHeader.HOST.asString(), "localhost"); request.setHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.FORM_ENCODED.asString()); request.setURI("/test"); request.setContent("name=Jetty".getBytes(StandardCharsets.UTF_8)); HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(tester.getResponses(request.generate()))); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); assertThat( new String(response.getContentBytes(), StandardCharsets.UTF_8) ).isEqualTo("Hello Jetty!!"); tester.stop(); } }
コード上でServerとLocalConnectorが登場しなくなりますが、ServerもLocalConnectorもそれぞれ取得することはできます。
ServletTester tester = new ServletTester(); ServletHandler handler = new ServletHandler(); handler.addServletWithMapping(TestServlet.class, "/test"); tester.getServer().setHandler(handler);
startするのは、ServletTesterになります、と。
tester.start();
stopするのも、ServletTesterのみです。
tester.stop();
リクエストを投げ込むには、ServletTester#getResponses(複数形)を使います。それ以外のイメージは、LocalConnectorの時と同じです。
HttpTester.Request request = HttpTester.newRequest(); request.setHeader(HttpHeader.HOST.asString(), "localhost"); request.setURI("/test?name=Jetty"); HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(tester.getResponses(request.generate())));
先のServer+LocalConnectorを使ったコードを見ておくと、そう違和感ない感じでしょう。
なお、ServletTesterには、ふつうのServerConnectorを作成するようにすることもできます。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/ServletTester.java#L235-L249
使うのかな…?
ServletTesterの存在自体は、Jettyのテストコードを見ていて気付きました。
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/DispatchServletTest.java
https://github.com/eclipse/jetty.project/blob/jetty-9.4.8.v20171121/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HeaderFilterTest.java
速度?
実行速度はこんな感じ。
コンパイル済みの「mvn test」の結果。
------------------------------------------------------- T E S T S ------------------------------------------------------- Running org.littlewings.embedded.jetty.JettyLocalConnectorTest 2018-02-18 13:22:12.824:INFO::main: Logging initialized @428ms to org.eclipse.jetty.util.log.StdErrLog 2018-02-18 13:22:12.928:INFO:oejs.Server:main: jetty-9.4.8.v20171121, build timestamp: 2017-11-22T06:27:37+09:00, git hash: 82b8fb23f757335bb3329d540ce37a2a2615f0a8 2018-02-18 13:22:12.956:INFO:oejs.AbstractConnector:main: Started LocalConnector@78ac1102{HTTP/1.1,[http/1.1]} 2018-02-18 13:22:12.957:INFO:oejs.Server:main: Started @561ms 2018-02-18 13:22:13.090:INFO:oejs.AbstractConnector:main: Stopped LocalConnector@78ac1102{HTTP/1.1,[http/1.1]} 2018-02-18 13:22:13.098:INFO:oejs.Server:main: jetty-9.4.8.v20171121, build timestamp: 2017-11-22T06:27:37+09:00, git hash: 82b8fb23f757335bb3329d540ce37a2a2615f0a8 2018-02-18 13:22:13.100:INFO:oejs.AbstractConnector:main: Started LocalConnector@55a561cf{HTTP/1.1,[http/1.1]} 2018-02-18 13:22:13.101:INFO:oejs.Server:main: Started @705ms 2018-02-18 13:22:13.103:INFO:oejs.AbstractConnector:main: Stopped LocalConnector@55a561cf{HTTP/1.1,[http/1.1]} Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.314 sec - in org.littlewings.embedded.jetty.JettyLocalConnectorTest Running org.littlewings.embedded.jetty.JettyServletTest 2018-02-18 13:22:13.137:INFO:oejs.Server:main: jetty-9.4.8.v20171121, build timestamp: 2017-11-22T06:27:37+09:00, git hash: 82b8fb23f757335bb3329d540ce37a2a2615f0a8 2018-02-18 13:22:13.139:INFO:oejs.AbstractConnector:main: Started LocalConnector@27ae2fd0{HTTP/1.1,[http/1.1]} 2018-02-18 13:22:13.140:INFO:oejs.Server:main: Started @744ms 2018-02-18 13:22:13.143:INFO:oejs.AbstractConnector:main: Stopped LocalConnector@27ae2fd0{HTTP/1.1,[http/1.1]} 2018-02-18 13:22:13.146:INFO:oejs.Server:main: jetty-9.4.8.v20171121, build timestamp: 2017-11-22T06:27:37+09:00, git hash: 82b8fb23f757335bb3329d540ce37a2a2615f0a8 2018-02-18 13:22:13.149:INFO:oejs.AbstractConnector:main: Started LocalConnector@7a4ccb53{HTTP/1.1,[http/1.1]} 2018-02-18 13:22:13.149:INFO:oejs.Server:main: Started @754ms 2018-02-18 13:22:13.151:INFO:oejs.AbstractConnector:main: Stopped LocalConnector@7a4ccb53{HTTP/1.1,[http/1.1]} Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.022 sec - in org.littlewings.embedded.jetty.JettyServletTest Results : Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
もうちょっと重たいJetty構成(jetty-webappとか)でやったらよかったかも、と後で思いました…。
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.314 sec - in org.littlewings.embedded.jetty.JettyLocalConnectorTest Running org.littlewings.embedded.jetty.JettyServletTest Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.022 sec - in org.littlewings.embedded.jetty.JettyServletTest
JSPとか入ると、一気に重くなるみたいですしねぇ…。