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