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;
  }
}