Guice:ユニットテストでのSessionScoped、RequestScoped
問題
SessionScopedやRequestScopedは非常に便利なのだが、それらを使ったコードをユニットテスト対象にできない。ユニットテスト上では、これらのスコープが存在しないからだ。以下のようなエラーが発生する。
1) No scope is bound to com.google.inject.servlet.RequestScoped.
  at foo.bar.FooBar.class(FooBar.java:55)
  while locating com.google.inject.Provider<foo.bar.FooBar>
    for field at foo.bar.Sample.fooBarProvider(Sample.java:32)
  at foo.bar.Sample.configure(Sample.java:52)
1 error
    at com.google.inject.internal.Errors.throwCreationExceptionIfErrorsExist(Errors.java:543)
    at com.google.inject.internal.InternalInjectorCreator.initializeStatically(InternalInjectorCreator.java:159)
    at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:106)
    at com.google.inject.Guice.createInjector(Guice.java:87)
    at com.google.inject.Guice.createInjector(Guice.java:69)
解決
これについての議論は以下にある。
が、あまり役にはたたないようだ。
結局のところ、以下のようにしてスコープを定義してしまえばよい。
public class ScopeTest {
  public static final AbstractModule REQUEST_SESSION_SCOPE = new AbstractModule() {
    protected void configure() { 
      Scope scope = new Scope() { 
        public <T> Provider<T> scope(Key<T> key, final Provider<T> provider) { 
          return ()->provider.get();
        }
      };
      bindScope(RequestScoped.class, scope);
      bindScope(SessionScoped.class, scope);
    } 
  };
  @RequestScoped
  public static class Person {} 
  @SessionScoped
  public static class Sample {}
  public static void main(String[] args) {     
    Injector injector = Guice.createInjector(REQUEST_SESSION_SCOPE); 
    System.out.println(injector.getInstance(Person.class)); 
    System.out.println(injector.getInstance(Sample.class));    
  } 
}
当然ながら、RequestScoped, SessionScopedの役目は一切果たさないが、ユニットテストではこれで事足りる。
HttpServletRequest等もサポートする
実は上記では足りなかった。RequestScoped, SessionScopedではHttpServletRequest等もInjectされるため、これらもサポートしなくてはいけない。
import java.lang.reflect.*;
import java.util.*;
import com.google.inject.*;
import com.google.inject.servlet.*;
/**
 * ダミーのSessionScoped, RequestScopedをサポートし、
 * ダミーのHttpServletRequestを作成する。
 * 本モジュールは本番のサーブレット環境で使用しては行けない。
 * ユニットテストやJavaアプリでの未使用すること。
 * <p>
 * サーブレット環境では、SessionScoped, RequestScopedが正しく定義されるが、
 * しかし、ユニットテストやJavaアプリでこれらのアノテーションの付加されたクラスを使おうとすると、
 * 以下のエラーになる。
 * </p>
 * <pre><code>
1) No scope is bound to com.google.inject.servlet.RequestScoped.
  at foo.bar.FooBar.class(FooBar.java:55)
  while locating com.google.inject.Provider<foo.bar.FooBar>
    for field at foo.bar.Sample.fooBarProvider(Sample.java:32)
  at foo.bar.Sample.configure(Sample.java:52)
1 error
    at com.google.inject.internal.Errors.throwCreationExceptionIfErrorsExist(Errors.java:543)
    at com.google.inject.internal.InternalInjectorCreator.initializeStatically(InternalInjectorCreator.java:159)
    at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:106)
    at com.google.inject.Guice.createInjector(Guice.java:87)
    at com.google.inject.Guice.createInjector(Guice.java:69)
 * </code></pre>
 * <p>
 * つまり、RequestScoped, SessionScopedが定義されていないためである。
 * </p>
 * <p>
 * 本モジュールではユニットテストやJavaアプリで作成するGuiceインジェクタに強制的に
 * それらのスコープを定義してしまうものであるが、
 * 当然のことながら、本来のRequestScoped, SessionScopedのスコープにはならない。
 * 単純に「スコープ無し」のオブジェクトが作成される。
 * </p>
 * <p>
 * しかし、SessionScoped, RequestScopedのサポートだけでは十分ではない。
 * そこでは、HttpServletRequest等がInjectされるため、これらのサポートも行わねばならない。
 * ここでは、単純に何もしないダミーのプロキシオブジェクトを作成している。
 * </p>
 */
public class ScopesDummyModule extends AbstractModule {
  /** ダミーオブジェクトを作成する対象クラス */
  static Class<?>[]CLASSES = new Class[] {
    javax.servlet.http.HttpSession.class,   
    javax.servlet.http.HttpServletRequest.class,
    javax.servlet.http.HttpServletResponse.class,
    javax.servlet.ServletRequest.class,
    javax.servlet.ServletResponse.class,
    javax.servlet.ServletContext.class,
  };
  /** モジュールのコンフィギュレーション */
  @SuppressWarnings({ "rawtypes", "unchecked" })
  @Override
  protected void configure() { 
    // スコープオブジェクトを作成する
    Scope scope = new Scope() { 
      public <T> Provider<T> scope(Key<T> key, final Provider<T> provider) { 
        return ()->provider.get();
      }
    };
    // RequestScoped, SessionScopedにバインドする
    bindScope(RequestScoped.class, scope);
    bindScope(SessionScoped.class, scope);
    // その他、サーブレット環境でのみInjectされるクラスについて、そのプロキシを作成
    Arrays.stream(CLASSES).forEach(clazz-> {
      bind(clazz).toProvider(new Provider() {
        @Override
        public Object get() {
          // プロキシオブジェクトを作成する
          return Proxy.newProxyInstance(
            ScopesDummyModule.class.getClassLoader(), 
            new Class[] {  clazz },
            new ProxyObject(clazz)
          );
        }        
      });
    });
  } 
  /**
   * 任意のクラスのプロキシオブジェクト。
   * {@link Object#toString()}では名称を返す。
   * それ以外のメソッド呼び出しでは、何もせずnullを返す。
   */
  static class ProxyObject implements InvocationHandler { 
    final Class<?>clazz;
    ProxyObject(Class<?>clazz) {
      this.clazz = clazz;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
      throws Throwable { 
      if (method.getName().equals("toString") &&
          method.getParameterCount() == 0) {
        return clazz.getName() + " Proxy";
      }
      return null;
    }
  }
}
上のモジュールを以下のようにして使う。
import static org.junit.Assert.*;
import javax.servlet.http.*;
import org.junit.*;
import com.google.inject.*;
import com.google.inject.servlet.*;
/**
 * サーブレット環境に無い場合でも、RequestScoped, SessionScopedをハンドリングし、
 * HttpServletRequestのダミーオブジェクトを作成する
 */
public class ScopesDummyModuleTest {
  Injector injector;
  @Before
  public void before() {
    injector = Guice.createInjector(new ScopesDummyModule());    
  }
  @Test
  public void test() {    
    Foo foo = injector.getInstance(Foo.class);
    assertNotNull(foo.req);
    Bar  bar = injector.getInstance(Bar.class);
    assertNotNull(bar.res);
  }
  @RequestScoped
  public static class Foo {
    @Inject HttpServletRequest req;
  }
  @SessionScoped
  public static class Bar {
    @Inject HttpServletResponse res;
  }
}





ディスカッション
コメント一覧
まだ、コメントがありません