ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

JerseyTest で Servlet のリソースを扱う JAX-RS 部品のユニットテストを行う方法

要約

前提知識 : ServletJAX-RS

Jakarta EE (旧 Java EE) における HTTP サーバーアプリケーションのための API として有名なもの *1ServletJAX-RS がある。 これらは依存関係にはなく、Servlet 単体でも JAX-RS 単体でも使用できる。

一方で、JAX-RSServlet コンテナ内で実行し、JAX-RS コンポーネントServletコンポーネントをインジェクトするというような使い方もできる。

In a product that also supports the Servlet specification, implementations MUST support JAX-RS applications that are packaged as a Web application. See Section 2.3.2 for more information Web application packaging.

JAX-RS: Java™ API for RESTful Web Services version 2.1 (11. Environments - 11.2.1 Servlets)

The @Context annotation can be used to indicate a dependency on a Servlet-defined resource. A Servlet-based implementation MUST support injection of the following Servlet-defined types: ServletConfig, ServletContext, HttpServletRequest and HttpServletResponse.

JAX-RS: Java™ API for RESTful Web Services version 2.1 (11. Environment - 11.1 Servlet Container)

Servlet のリソースを扱う JAX-RS コンポーネントユニットテストする方法 (JerseyTest を使用)

JAX-RS コンポーネントユニットテストする方法は様々あるだろうが、個人的には JAX-RS の参照実装である Jersey のテストフレームワークを使う方法に親しみがある。

Jersey のテストフレームワークJerseyTest というクラスを提供しており、これを継承してテストクラスを作ると簡単に JAX-RS コンポーネントのテストを記述できる。

public class SimpleTest extends JerseyTest {
    // SimpleResource という JAX-RS リソースをテストしたい場合は、
    // それを ResourceConfig に渡してテスト用の Application を作ればよい。
    @Override
    protected Application configure() {
        return new ResourceConfig(SimpleResource.class);
    }

    // テストメソッド 
    @Test
    public void test() {
        final String responseContent = target("simple").request().get(String.class);
        assertEquals("Hello World!", responseContent);
    }
}

問題 : 単に JerseyTest を継承してテストクラスを定義するだけだと Servlet リソースが扱われない

上のようなテストコードを書いた場合、基本的には問題なくテストが動く。 しかし、SimpleResourceServlet のリソースを扱っている場合 (@ContextHttpServletRequest をインジェクトしている場合など) には、期待通りに動かない。

なぜなら、(おそらく) デフォルトで選択されるテストコンテナは Servlet をサポートしておらず、Servlet のリソースのインジェクトが行われないからである。

解決策 : Servlet をサポートしているテストコンテナを利用する

テストコンテナの決まり方については、ドキュメントに下記のように書かれている。

A test container is selected based on various inputs. JerseyTest#getTestContainerFactory() is always executed, so if you override it and provide your own version of TestContainerFactory, nothing else will be considered. Setting a system variable TestProperties#CONTAINER_FACTORY has similar effect. This way you may defer the decision on which containers you want to run your tests from the compile time to the test execution time. Default implementation of TestContainerFactory looks for container factories on classpath. If more than one instance is found and there is a Grizzly test container factory among them, it will be used; if not, a warning will be logged and the first found factory will be instantiated.

Chapter 26. Jersey Test Framework

一番簡単な方法は JerseyTest#getTestContainerFactory() をオーバーライドすることだろう。

Servlet をサポートしているテストコンテナもドキュメントに書かれている。

Second factory is GrizzlyWebTestContainerFactory that is Servlet-based and supports Servlet deployment context for tested applications. This factory can be useful when testing more complex Servlet-based application deployments.

Chapter 26. Jersey Test Framework

GrizzlyWebTestContainerFactory を使えばよい。 ちなみに GrizzlyWebTestContainerFactoryDeploymentContext として ServletDeploymentContext を求めるので、その点は注意が必要である。

ということで、下記のようなテストクラスを書けば Servlet のリソースを扱う JAX-RSユニットテストを記述できる。

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.ServletDeploymentContext;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.glassfish.jersey.test.spi.TestContainerException;
import org.glassfish.jersey.test.spi.TestContainerFactory;

public class CookieSessionResponseFilterTest extends JerseyTest {

    private TestContainerFactory testContainerFactory;

    /**
     * テスト用のコンテナを生成するメソッドをオーバーライド。
     *
     * ここで {@link GrizzlyWebTestContainerFactory} を指定することで、サーブレットベースのアプリケーションのテストが可能となる。
     */
    @Override
    protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
        // こういう感じで同じインスタンスを返す必要があるのかはよくわからないが
        // JerseyTest の実装がそういう感じになっているのでそれに合わせている。
        if (testContainerFactory == null) {
            testContainerFactory = new GrizzlyWebTestContainerFactory();
        }
        return testContainerFactory;
    }

    /**
     * テスト用のコンテナにデプロイする内容。
     *
     * {@link GrizzlyWebTestContainerFactory} を使うためには {@link ServletDeploymentContext} を返す必要がある。
     */
    @Override
    protected ServletDeploymentContext configureDeployment() {
        ServletContainer jaxRsServlet = ;
        return ServletDeploymentContext
                // JAX-RS コンポーネントは `ServletContainer` に包んでやる。
                .forServlet(new ServletContainer(configure()))
                // Servlet フィルタの追加も可能。
                .addFilter(TestServletFilter.class, TestServletFilter.class.getSimpleName())
                .build();
    }

    // SimpleResource という JAX-RS リソースをテストしたい場合は、
    // それを ResourceConfig に渡してテスト用の Application を作ればよい。
    // {@link #configureDeployment()} をオーバーライドしたらこのメソッドを
    // オーバーライドしなくても良いはず (configureDeployment 内に書けばよいはず) だが、
    // 一応オーバーライドして {@link #configureDeployment()} から呼ぶようにした。
    @Override
    protected ResourceConfig configure() {
        return new ResourceConfig(SimpleResource.class);
    }

    // テストメソッド 
    @Test
    public void test() {
        final String responseContent = target("simple").request().get(String.class);
        assertEquals("Hello World!", responseContent);
    }
}

自分でテストコンテナのファクトリを書いても良い

上ではもともと Jersey のテストフレームワークに用意されている GrizzlyWebTestContainerFactory を利用したが、独自に TestContainerFactory を実装しても良い。 サーブレットを複数含むテストを書きたい場合などにはそうする必要がありそうである。 (そもそもそういうテストを書く必要がある設計にすべきではないという気もするが。)

JerseyTestでHttpServletRequestを使いたい - Chonaso's Commentary』 が参考になる。 (実際に自分で書いたらわりと手を入れる必要はあったが。)

おわり

まとめると、下記のとおり。

  • Jersey のテストフレームワークが便利。
  • Servlet のリソースも含めてテストしたい場合には GrizzlyWebTestContainerFactory を使用すると良い。

とはいえ、JAX-RSServlet のリソースを扱う必要がある場面は非常に限定的であるはずで、JAX-RSAPI だけで完結できる場面ではそうすべきである。 その方がテストも書きやすいし移植性も高い。 複数の API 仕様の知識 (JAX-RS の知識も Servelt の知識も必要) を求めることもなくなるので、開発者に対しても優しいはずである。

仕事で触っているコードには JAX-RS の中で Servlet のリソースを扱っているものがわりとあるのだが、少しずつ JAX-RSAPI に置き換えていきたい。

*1:これ以外にあるのかどうかは知らない。