JerseyTest で Servlet のリソースを扱う JAX-RS 部品のユニットテストを行う方法
要約
前提知識 : Servlet と JAX-RS
Jakarta EE (旧 Java EE) における HTTP サーバーアプリケーションのための API として有名なもの *1 は Servlet と JAX-RS がある。 これらは依存関係にはなく、Servlet 単体でも JAX-RS 単体でも使用できる。
一方で、JAX-RS を Servlet コンテナ内で実行し、JAX-RS コンポーネントに Servlet のコンポーネントをインジェクトするというような使い方もできる。
- Servlet 4.0 : The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 369
- JAX-RS 2.1 : The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 370
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
JAX-RS: Java™ API for RESTful Web Services version 2.1 (11. Environment - 11.1 Servlet Container)@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
andHttpServletResponse
.
Servlet のリソースを扱う JAX-RS コンポーネントをユニットテストする方法 (JerseyTest を使用)
JAX-RS コンポーネントをユニットテストする方法は様々あるだろうが、個人的には JAX-RS の参照実装である Jersey のテストフレームワークを使う方法に親しみがある。
- Jersey のテストフレームワーク : Chapter 26. Jersey Test Framework
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 リソースが扱われない
上のようなテストコードを書いた場合、基本的には問題なくテストが動く。 しかし、SimpleResource
が Servlet のリソースを扱っている場合 (@Context
で HttpServletRequest
をインジェクトしている場合など) には、期待通りに動かない。
なぜなら、(おそらく) デフォルトで選択されるテストコンテナは Servlet をサポートしておらず、Servlet のリソースのインジェクトが行われないからである。
解決策 : Servlet をサポートしているテストコンテナを利用する
テストコンテナの決まり方については、ドキュメントに下記のように書かれている。
A test container is selected based on various inputs.
Chapter 26. Jersey Test FrameworkJerseyTest#getTestContainerFactory()
is always executed, so if you override it and provide your own version ofTestContainerFactory
, nothing else will be considered. Setting a system variableTestProperties#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 ofTestContainerFactory
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.
一番簡単な方法は JerseyTest#getTestContainerFactory()
をオーバーライドすることだろう。
Servlet をサポートしているテストコンテナもドキュメントに書かれている。
Second factory is
Chapter 26. Jersey Test FrameworkGrizzlyWebTestContainerFactory
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.
GrizzlyWebTestContainerFactory
を使えばよい。 ちなみに GrizzlyWebTestContainerFactory
は DeploymentContext
として 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』 が参考になる。 (実際に自分で書いたらわりと手を入れる必要はあったが。)
おわり
まとめると、下記のとおり。
とはいえ、JAX-RS で Servlet のリソースを扱う必要がある場面は非常に限定的であるはずで、JAX-RS の API だけで完結できる場面ではそうすべきである。 その方がテストも書きやすいし移植性も高い。 複数の API 仕様の知識 (JAX-RS の知識も Servelt の知識も必要) を求めることもなくなるので、開発者に対しても優しいはずである。
仕事で触っているコードには JAX-RS の中で Servlet のリソースを扱っているものがわりとあるのだが、少しずつ JAX-RS の API に置き換えていきたい。
*1:これ以外にあるのかどうかは知らない。