CLOVER🍀

That was when it all began.

テスト用に組み込みTomcat、JDK HttpServerを使う

WebアプリやHTTP通信のテストコード用に、簡易なサーバが欲しいと思いまして。

以下の2つを使うことを考えてみます。

  • Servletなどを動かすためのサーバー
  • 静的ファイルなどを読むための、簡易的なHTTPサーバー

このあたりを、組み込みTomcatとJDKに付いているHttpServerを使って作ってみたいと思います。

また、目的が目的なので、テストコードと組み合わせて考えます。

Servletなどを動かすためのサーバー

これは、作成しているWebアプリケーション自身をデプロイして動かすことを考えます。

この時、web.xmlを読ませるようにしておきます。
※単純に、後で方法を探すのが面倒なので

Mavenで使うpom.xmlは、このように構成。
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>webapp-test-embedded-tomcat-jdkserver</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.0.28</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>8.0.28</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-logging-juli</artifactId>
            <version>8.0.28</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.2.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

簡単なServletも用意します。
src/main/java/org/littlewings/web/HelloServlet.java

package org.littlewings.web;

import java.io.IOException;
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 HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String name = Optional.ofNullable(request.getParameter("name")).orElse("World");
        response.getWriter().write("Hello " + name + "!!");
    }
}

web.xmlも用意。
src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>org.littlewings.web.HelloServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

では、組み込みTomcatをテストコードで使うことを考えます。単純に、組み込みTomcatのラッパーを用意。
src/test/java/org/littlewings/web/EmbeddedTomcat.java

package org.littlewings.web;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import javax.servlet.ServletException;

import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;

public class EmbeddedTomcat {
    private Tomcat tomcat;
    private boolean temporaryBaseDir = false;
    private String baseDir;
    private int port;
    private Context context;

    protected EmbeddedTomcat(Tomcat tomcat) {
        this.tomcat = tomcat;
    }

    public static EmbeddedTomcat create() {
        return new EmbeddedTomcat(new Tomcat());
    }

    public static EmbeddedTomcat create(Tomcat tomcat) {
        return new EmbeddedTomcat(tomcat);
    }

    public EmbeddedTomcat port(int port) {
        tomcat.setPort(port);
        this.port = port;
        return this;
    }

    public EmbeddedTomcat temporaryBaseDir() {
        temporaryBaseDir = true;
        return baseDir(createTemporaryPath("tomcat", Integer.toString(port)));
    }

    public EmbeddedTomcat baseDir(String path) {
        tomcat.setBaseDir(path);
        baseDir = path;
        return this;
    }

    public EmbeddedTomcat webapp(String contextPath, String docBase) {
        try {
            if (context == null) {
                context = tomcat.addWebapp(contextPath, docBase);
            } else {
                throw new IllegalArgumentException("Only support, one webapp.");
            }
            return this;
        } catch (ServletException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public EmbeddedTomcat start() {
        try {
            tomcat.start();
            return this;
        } catch (LifecycleException e) {
            throw new IllegalStateException(e);
        }
    }

    public EmbeddedTomcat await() {
        tomcat.getServer().await();
        return this;
    }

    public void shutdown() {
        try {
            tomcat.stop();
            tomcat.destroy();

            if (temporaryBaseDir) {
                Files.walkFileTree(Paths.get(baseDir), new SimpleFileVisitor<Path>() {
                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
                        Files.delete(file);
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
                        Files.delete(directory);
                        return FileVisitResult.CONTINUE;
                    }
                });
            }
        } catch (LifecycleException | IOException e) {
            throw new IllegalStateException(e);
        }
    }

    public static String toAbsolutePath(String path) {
        return Paths.get(path).toAbsolutePath().toString();
    }

    public static String createTemporaryPath(String prefix, String suffix) {
        try {
            File tempDir = File.createTempFile(prefix, suffix);
            tempDir.delete();
            tempDir.mkdir();
            tempDir.deleteOnExit();
            return tempDir.getAbsolutePath();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

TomcatのdocBaseは一時領域にもできて、その場合は終了時に削除するようにしています。Webアプリケーションは、ひとつのみで考えています。この時に、src/main/webappとかをWebアプリケーションのdocBaseとして設定します。

実際に、このコードを使ったテストコード。
src/test/java/org/littlewings/web/HelloServletTest.java

package org.littlewings.web;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class HelloServletTest {
    private static EmbeddedTomcat tomcat;

    @BeforeClass
    public static void setUpClass() {
        tomcat = EmbeddedTomcat
                .create()
                .port(8080)
                .temporaryBaseDir()
                .webapp("/", EmbeddedTomcat.toAbsolutePath("src/main/webapp"))
                .start();
    }

    @AfterClass
    public static void tearDownClass() {
        tomcat.shutdown();
    }

    @Test
    public void testServlet1() throws IOException {
        HttpURLConnection conn =
                (HttpURLConnection) URI
                        .create("http://localhost:8080/hello?name=Tomcat")
                        .toURL()
                        .openConnection();
        try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8);
             BufferedReader reader = new BufferedReader(isr)) {
            assertThat(reader.readLine())
                    .isEqualTo("Hello Tomcat!!");
        } finally {
            conn.disconnect();
        }
    }

    @Test
    public void testServlet2() throws IOException {
        HttpURLConnection conn =
                (HttpURLConnection) URI
                        .create("http://localhost:8080/hello")
                        .toURL()
                        .openConnection();
        try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8);
             BufferedReader reader = new BufferedReader(isr)) {
            assertThat(reader.readLine())
                    .isEqualTo("Hello World!!");
        } finally {
            conn.disconnect();
        }
    }
}

これで、web.xmlを認識しつつServletを組み込みTomcatで動かして、テストができました、と。

静的ファイルなどを読むための、簡易的なHTTPサーバー

今度は、HTMLファイルなどを静的に読み込むサーバーを書いてみます。

こちらは、JDKに付属しているHttpServerを使います。

こんな感じで実装してみました。
src/test/java/org/littlewings/web/StaticHttpServer.java

package org.littlewings.web;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.Executor;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class StaticHttpServer {
    private HttpServer httpServer;
    private String contextPath;
    private HttpHandler handler;

    protected StaticHttpServer(HttpServer httpServer, String contextPath, String docRoot) {
        this.httpServer = httpServer;
        this.contextPath = contextPath;
        handler = defaultHandler(docRoot);
    }

    public static StaticHttpServer create(int port, String contextPath, String docRoot) {
        try {
            return new StaticHttpServer(HttpServer.create(new InetSocketAddress(port), 0), contextPath, docRoot);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public StaticHttpServer executor(Executor executor) {
        httpServer.setExecutor(executor);
        return this;
    }

    public StaticHttpServer handler(HttpHandler handler) {
        this.handler = handler;
        return this;
    }

    protected HttpHandler defaultHandler(String docRoot) {
        Path docRootPath = Paths.get(docRoot);

        return exchange -> {
            URI uri = exchange.getRequestURI();
            String path = uri.getPath();
            String relativePath = path.substring(1);
            Path requestPath = docRootPath.resolve(relativePath);

            if (Files.exists(requestPath) && requestPath.startsWith(docRoot)) {
                byte[] bytes = Files.readAllBytes(requestPath);
                exchange.getResponseHeaders().add("Content-Type", "text/html;charset=utf-8");
                exchange.sendResponseHeaders(200, bytes.length);
                try (OutputStream os = exchange.getResponseBody()) {
                    for (byte b : bytes) {
                        os.write(b);
                    }
                }
            } else {
                byte[] messageBinary = ("Request[" + uri + "], Not Found.").getBytes(StandardCharsets.UTF_8);
                exchange.sendResponseHeaders(404, messageBinary.length);
                try (OutputStream os = exchange.getResponseBody()) {
                    for (byte b : messageBinary) {
                        os.write(b);
                    }
                }
            }
        };
    }

    public StaticHttpServer start() {
        httpServer.createContext(contextPath, handler);
        httpServer.start();
        return this;
    }

    public void shutdown() {
        shutdown(0);
    }

    public void shutdown(int waitSeconds) {
        httpServer.stop(waitSeconds);
    }
}

ドキュメントルートやコンテキストパス、ポートを指定して起動します。Content-Typeはtext/html決め打ちですが、拡張子などである程度動的に変えてもいいでしょう。

とりあえず、簡易に、簡易に。

こちらを使った、テストコード。
src/test/java/org/littlewings/web/StaticHttpServerTest.java

package org.littlewings.web;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class StaticHttpServerTest {
    private static StaticHttpServer httpServer;

    @BeforeClass
    public static void setUpClass() {
        httpServer =
                StaticHttpServer
                        .create(10000, "/", "src/main/webapp")
                        .start();
    }

    @AfterClass
    public static void tearDownClass() {
        httpServer.shutdown();
    }

    @Test
    public void testExistContents() throws IOException {
        HttpURLConnection conn =
                (HttpURLConnection) URI
                        .create("http://localhost:10000/WEB-INF/web.xml")
                        .toURL()
                        .openConnection();
        try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8);
             BufferedReader reader = new BufferedReader(isr)) {
            assertThat(conn.getHeaderField("Content-Type"))
                    .isEqualTo("text/html;charset=utf-8");

            assertThat(reader.readLine())
                    .isEqualTo("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        } finally {
            conn.disconnect();
        }
    }

    @Test(expected = FileNotFoundException.class)
    public void testNotFound() throws IOException {
        HttpURLConnection conn =
                (HttpURLConnection) URI
                        .create("http://localhost:10000/WEB-INF/notFound.xml")
                        .toURL()
                        .openConnection();
        try (InputStreamReader isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8);
             BufferedReader reader = new BufferedReader(isr)) {
            assertThat(reader.readLine())
                    .isEqualTo("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        } finally {
            conn.disconnect();
        }
    }
}

こんな感じで。

個人的に、ちょっと活用すると思います。