タグ「CDI」が付けられているもの

この記事は GlassFish Advent Calendar 2014 の 9 日目です。昨日は「GlassFish 4.1で始めるWebサービス&MQ通信―(1)JMS」です(都合により 8 日目と 9 日目を入れ替えました)。

今回は GlassFish 4.1 向けに JAX-RS の単体テストを行う上で知っておくと便利なテクニックについてお話します。GlassFish 4.1 向け、と銘打ってはいますが、Jersey 2.5.1 をバンドルしている WebLogic 12c (12.1.3) でもライブラリの依存関係解決方法は異なるものの、主要なコードは適用できるはずです。

GlassFish の JAX-RS 実装である Jersey には、Jersey Test Framework と呼ばれるテストツールが備わっており、JUnit と組み合わせることでリソースクラスの単体テストを行うことができます。

JAX-RS 2.0 では CDI との連携が追加され、リソースクラス内で @Inject アノテーションを用いた DI が可能になりました。しかし Jersey Testing Framework は CDI をサポートしないため、そのままではテストを実行することができません。Weld SE を利用して JUnit に CDI サポートを追加するライブラリも存在しますが、Jersey Test Frameworkと相性が悪いようで組み合わせることができません。

幸いなことに、Jersey は依存ライブラリー HK2 の機能を利用することで、手動で @Inject を解決することができます。今回は HK2 の機能を利用してCDIと連携するリソースクラスの単体テストを行う方法をご紹介します。HK2 は GlassFish の主要な構成要素であるため IDE がパスを解決さえできれば他に何も要らないというメリットがあります。また、HK2 による @Inject の解決は Google Guice のbind と似ていますので、Guice の利用経験があれば容易に理解できるでしょう。

参考まで、Jersey 2.x と CDI (Weld 2.x) の連携には HK2 が関わっています。Jersey 2.x の場合 JAX-RS Client API を使うだけで HK2 をはじめ多くのライブラリが一緒に付いてくるのは、このような構成によるものです。

では、具体例を見てみましょう。事前準備として、pom.xml に以下の依存関係を追加しておいてください。

<dependencies>
  ...
  <!-- JUnit -->
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-core</artifactId>
    <version>1.3</version>
    <scope>test</scope>
  </dependency>
  
  <!-- Jersey Test Framework -->
  <dependency>
    <groupId>org.glassfish.jersey.test-framework.providers</groupId>
    <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
    <version>2.10.4</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jsonp-jaxrs</artifactId>
    <version>1.0</version>
    <scope>test</scope>
  </dependency>
  
  <!-- HK2 -->
  <dependency>
    <groupId>org.glassfish.hk2</groupId>
    <artifactId>hk2-api</artifactId>
    <version>2.3.0-b07</version>
  </dependency>
  ...
</dependencies>

Eclipse を利用している場合は、Eclipse 側のビルド・パス解決で JUnit のライブラリを含めてしまうことも多いため、pom.xml に JUnit の依存関係は入れなくても動きます。同じことは HK2 にも当てはまります。ただし、他の IDE でテストを実行することも考慮してサボらずに依存関係を明記します。

この例では jsonp-jaxrs(JSON-P の JAX-RS 連携モジュール)を依存関係に含めています。ドキュメントにはなかったのですが、これがないと jersey-test-framework-provider-grizzly2 が動作しなかったため、おまじないのつもりで追加しています。ドキュメントと実装のどちらが間違っているのかは不明ですが。

次にテスト対象のリソースクラスです。このクラスは Twitter 検索実行のファサードとなるもので、クエリー・パラメーターに検索文字列と最大取得件数を指定しています。

package jp.coppermine.examples.twitter;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;

import java.util.List;

import javax.enterprise.context.RequestScoped;
import javax.validation.constraints.NotNull;
import javax.ws.rs.DefaultValue;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import jp.coppermine.examples.twitter.Tweet;

@Path("/twitter")
@RequestScoped
public class TwitterResource {
  
  // TweetFinder の実装が Twitter 検索を実行する。
  // 検索結果は TwitterFinder#search(String, int) で取得する。
  @Inject
  private TweetFinder finder;
  
  @GET
  @Path("/search")
  @Produces({APPLICATION_XML, APPLICATION_JSON})
  public List<Tweet> search(@NotNull @QueryParam("q") String keyword, @DefaultValue("15") @QueryParam("count") int count) {
    List<Tweet> tweets = finder.search(keyword, count);
    return tweets.size() > count ? tweets.subList(0, count) : tweets;
  }
}

なお、上記 TweetFinder は以下のようなインタフェースです。

package jp.coppermine.examples.twitter;

import java.util.List;

import jp.coppermine.examples.twitter.Tweet;

public interface TweetFinder {
  
  List<Tweet> search(String keyword, int count);

}

今回のテストでは実際に Twitter API を呼び出す Stateless Bean 実装ではなく、あらかじめ用意したテストデータを返すモックアップ(TweetFinderMock)を使うこととします。

続いて、リソースクラスの単体テストクラスを以下に示します。

package jp.coppermine.example.twitter;

import static javax.ws.rs.core.MediaType.APPLICATION_XML;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.util.List;

import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Response;

import jp.coppermine.example.twitter.Tweet;

import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

public class TwitterResourceTest extends JerseyTest {
  
  /**
   * DI の解決ルールを記述したクラス
   */
  public static class TestBinder extends AbstractBinder {
    @Override
    protected void configure() {
      bind(TweetFinderMock.class).to(TweetFinder.class);
    }
  }
  
  /**
   * Application クラス(またはそのサブクラス)を返す。
   * ここでは Application のサブクラスである ResourceConfig クラスを用いて
   * TestBinder とテスト対象のリソースクラス(TwitterResource)を関連づけ、
   * TwitterResource の DI を解決する。
   */
  @Override
  protected Application configure() {
    return new ResourceConfig().register(new TestBinder()).register(TwitterResource.class);
  }
  
  @Test
  public void testSearch_count99() {
    List<Tweet> tweets = target("twitter").path("search").queryParam("q", "keyword1").queryParam("count", 99).request().accept(APPLICATION_XML).get(new GenericType<List<Tweet>>() { });
    assertThat(tweets.size(), is(99));
  }
  
  @Test
  public void testSearch_count100() {
    List<Tweet> tweets = target("twitter").path("search").queryParam("q", "keyword1").queryParam("count", 100).request().accept(APPLICATION_XML).get(new GenericType<List<Tweet>>() { });
    assertThat(tweets.size(), is(100));
  }
  
  @Test
  public void testSearch_count101() {
    List<Tweet> tweets = target("twitter").path("search").queryParam("q", "keyword1").queryParam("count", 101).request().accept(APPLICATION_XML).get(new GenericType<List<Tweet>>() { });
    assertThat(tweets.size(), is(101));
  }
  
  @Test
  public void testSearch_defaultCount() {
    List<Tweet> tweets = target("twitter").path("search").queryParam("q", "keyword1").request().accept(APPLICATION_XML).get(new GenericType<List<Tweet>>() { });
    assertThat(tweets.size(), is(15));
  }
  
  @Test
  public void testSearch_noKeyword() {
    Response response = target("twitter").path("search").queryParam("count", 100).request().get();
    assertThat(response.getStatus(), is(400));
  }
}

ポイントは、HK2 API の AbstractBinder のサブクラス TestBinder を作成して DI の関連づけルールを記述しておくことと、Application のサブクラスである ResourceConfig クラスを利用して TestBinder とテスト対象の TwitterResource クラスを関連づけ DI を解決しているところです。この辺りの仕組みは Google Guice と類似していますが、bind の source と destination の順序が異なるなどの差異があります。また、今回の例にはありませんが、実装とインタフェースの結びつけだけでなく、実装を取得するファクトリーとインタフェースの結びつけを記述することもできます。

最近は様々なテストツールが登場し、テストの効率が向上しています。しかし、すべての開発現場においてそれらのツールを導入できるとは限りません。そのような時に、今回ご紹介した付属ライブラリだけによる実現方法は有力な選択肢になることでしょう。

本稿執筆に当たっては、「Playing with JerseyTest (Jersey 2.5.1 and DI)」および筆者が同記事を元に実務で実装したテストクラスを参考にしました。

明日は「GlassFish 4.1で始めるWebサービス&MQ通信―(2)JAX-WS」を予定しています。