てきとうなメモ

本の感想とか技術メモとか

Jersey1.xでモックをインジェクトしてテスト

Jersey1.x自体にDIの機能があるので、それを使ってみる。

基本的には

package com.sun.jersey.spi.inject;
public interface InjectableProvider<A extends Annotation, C> {
    ComponentScope getScope();
    Injectable getInjectable(ComponentContext ic, A a, C c);
}

を実装してプロバイダとしてアノテーション(@Provider)すれば良いようだ。Aはインジェクトに利用するアノテーション、Cはインジェクト先の型である。

getScopeはper-requestスコープかsingletonスコープかを返す。getInjectableはインジェクトしたい値をInjectableで包んだものを返す。Injectableインターフェースは以下のようになっている。getValueで中身を取り出せる。

package com.sun.jersey.spi.inject;
public interface Injectable<T> {
    T getValue();
}

InjectableProviderを一から実装するのはちょっと面倒なので、抽象クラスのSingletonTypeInjectableProviderやPerRequestTypeInjectableProviderがある。

例えば

@Path("/users")
public class UserResource {

  @Context
  private UserService userService;
  ...
  @GET
  @Path("{id}")
  @Produces(MediaType.APPLICATION_JSON)
  public Response getUser(@PathParam("id") int id) {
    User user = userService.get(id);
    if (user == null) {
      throw new WebApplicationException(Status.NOT_FOUND);
    }
    return Response.ok(user).build();
  }

  ...
}

というようなリソースがあって、userServiceはDBにアクセスしているので、テスト時にはDBにアクセスしないモックをインジェクトしたい場合、こんな感じでJerseyTestの内部クラスとしてモック用のInjectableProviderを用意する

public class UserResourceTest extends JerseyTest {
  
  private static UserService mockService;

  @Provider
  public static class MockUserServiceProvider extends SingletonTypeInjectableProvider<Context, UserService> {

    public MockUserServiceProvider() {
      super(UserService.class, mockService);
    }
  }

  ...
}

SingletonTypeInjectableProviderは、InjectableProviderを一部実装した抽象クラスである。

public abstract class SingletonTypeInjectableProvider<A extends Annotation, T> 
    implements InjectableProvider<A, Type>, Injectable<T> {

    private final Type t;
    private final T instance;
        
    public SingletonTypeInjectableProvider(Type t, T instance) {
        this.t = t;
        this.instance = instance;
    }
    
    public final ComponentScope getScope() {
        return ComponentScope.Singleton;
    }
    
    public final Injectable<T> getInjectable(ComponentContext ic, A a, Type c) {
        if (c.equals(t)) {
            return this;
        } else
            return null;
    }

    public final T getValue() {
        return instance;
    }
}

スコープはsinngletonになっている。InjectableProviderかつInjectableになっており、getInjectableは型cがコンストラクタで登録したtであれば、自分自身をInjectableとして返し、InjectableとしてのgetValueはコンストラクタで登録したインスタンスを返す。よって、上記の例ではUserService型であれば、登録したmockServiceを返すことになる。

getInjectable/getValueの実装と、型パラメタがContextになっていることより、@Contextアノテーションをした、UserServiceクラスにmockServiceがインジェクトされることになる。

テストの中身は以下のようになる。テストフレームワークJUnit、モックライブラリとしてはMockitoを利用している。

  @Test
  public void testGet() {
    
    User user = new User();
    user.setId(1);
    user.setName("田中一郎");
    
    mockService = mock(UserService.class);
    when(mockService.get(1)).thenReturn(user);
    
    User got = resource().path("/users/1").get(User.class);
    
    assertThat(got.getId(), is(1));
    assertThat(got.getName(), is("田中一郎"));
  }

テストコードの中でmockServiceの動作を定義している。resource()...でサーバにアクセスすると、サーバ側のリソースオブジェクトにはmockServiceがインジェクトされており、テストコード中のモックの動作定義も反映されている。そのため、userService(mockService)はDBにはアクセスせずただuserオブジェクトを返すことになる。

インジェクション用のアノテーション(今回は@Context)をしたフィールドには対応するProviderがないとJerseyのサーバ起動時にエラーになってしまうため、本番用のProviderも用意しないといけない。その場合テストコード側は2つProviderが存在してしまい、JerseyTestがリソースやプロバイダを探す時にパッケージ以下をスキャンするような設定にしているとうまくいかなそうなので、今回はApplicationのサブクラスを使って個別にリソースクラスやプロバイダクラスを指定するようにしてみた。

public class UserResourceTest {
  ...
  public UserResourceTest() {
    super(
      new WebAppDescriptor.Builder(
        WebComponent.APPLICATION_CONFIG_CLASS, 
        UserApplicationTestApp.class.getName()
      ).build()
    );
  }
  ...
  public static class UserApplicationTestApp extends Application {

    @Override
    public Set<Object> getSingletons() {
      Set<Object> singletons = new HashSet<Object>();
      singletons.add(new UserResource());
      singletons.add(new MockUserServiceProvider());
      return singletons;
    }
  }
}