S2Junit、テスト対象コンポーネントの依存コンポーネントを後から設定した場合に正しくDIしてくれない事象の解決策

更新履歴

モチベーション

S2Junitでmockitoを使いたかったのですよ。コード的にはこんなのを想定していた。

@RunWith(Seasar2.class)
public class HogeLogicTest {
  HogeLogic hogeLogic; // app.diconでコンポーネントが登録されてる

  // HogeLogicの実装が依存しているコンポーネント。HogeLogicの生成時にDIされる。
  // app.diconで指定されているが、テストの際はこれをmockで置き換えたい。
  HogeDao hogeDao;

  // S2Junitが提供するテスト制御用インタフェース。
  TestContext ctx;

  @Before
  public void before() {
    // mockitoでHogeDaoのmockを生成
    hogeDao=mock(HogeDao.class);
    // コンポーネントとして登録
    ctx.register(hogdDao,"hogdDao");
  }

  @Test
  public test_xxx() {
    // 以上の設定により、mockを使ったテストが可能になる!!
    when(hogeDao.getValue()).thenReturn(null);
    hogeLogic.xxx();
  }

}

現象

テスト開始前にHogeLogicが自動でDIされるが、ctx.register()で登録した依存クラス(HogeDao)を反映してくれない。

理由


S2Container

S2ContainerS2ContainerImplS2Container#getComponent


(一)getComponentDef(ComponentDef)

(二)ComponentDef#getComponent()


ComponentDefComponentDefImplInstanceDefComponentDeployer#deploy*1/
AbstractAssembler
// AbstractAssembler.java L:86 
// AbstractAssembler#getArgs
 args[i] = getComponentDef().getContainer().getComponent(
                        argTypes[i]);



S2JunitS2TestMethodRunner#runMethod()


(一)setUpTestContext() 

(二)runBefore() before

(三)initContainer() (s2junit4.diconinclude)

(四)bindFields() DI

(五)runTest() 







(一)S2TestMethodRunner#setUpTestContext 

(一)SingletonS2ContainerTestContext使


(二)before

(三)initContainer() s2junit4.diconinclude

(一)s2junit4.dicon


(四)bindFields()DI


S2Container







(: S2)

mock(HogeDao)s2junit4.diconHogeLogic

解決方法

上記の理由により、テストクラスのフィールドインジェクション前に設定したコンポーネントを反映させるのは不可能っぽい。

S2Containerのコンポーネントルックアップの挙動はS2ContainerBehavior.setProviderを使ってフックすることができる。この機構を使って、一時的に上書き用の定義を最優先で見に行くようにした。

	// 上書きしたいコンポーネントを定義したコンテナ
	private S2Container containerForOverride;

	public <T> T getComponent(Class<T> componentClass) {
		S2ContainerBehavior
				.setProvider(new S2ContainerBehavior.DefaultProvider() {
					@Override
					public ComponentDef acquireFromGetComponent(
							S2Container container, Object key) {
						if (containerForOverride.hasComponentDef(key))
							return containerForOverride.getComponentDef(key);
						else
							return super
									.acquireFromGetComponent(container, key);
					}
				});
		final T component = componentClass.cast(SingletonS2Container
				.getComponent(componentClass));
		S2ContainerBehavior
				.setProvider(new S2ContainerBehavior.DefaultProvider());
		return component;
	}
	// コンポーネント取得時は、自動フィールドインジェクションやSingletonS2Containerを使用せず
	// getComponent経由で取得する。

古いコード(一応残しとくけど無駄に複雑なことやってるので使わないほうがいいですよ)



/** このコンテナに登録されたコンポーネントは、通常のDI設定を上書きする */
final S2Container containerForOverride = new S2ContainerImpl();

/**
 * 指定したクラスのモックを作成し、コンテナに登録してから返す
 * 
 * @param <T>
 *            対象の型
 * @param targetClass
 *            対象のクラス
 * @return モックされ、コンテナに登録されたオブジェクト
 */
private <T> T mockAndRegister(Class<T> targetClass) {
 final T mocked = mock(targetClass);
 containerForOverride.register(mocked, targetClass.getSimpleName());
 return mocked;
}

public <T> T getComponent(Class<T> componentClass) {
 // テスト環境が使用しているルートのS2Containerを取得
 final S2Container root = SingletonS2ContainerFactory.getContainer();
 // 生成対象コンポーネントの定義をルートコンテナから取得
 final ComponentDef def = root.getComponentDef(componentClass);
 // 生成時に依存コンポーネントを解決するのに使われるのは、コンポーネント定義が所有するコンテナ。
 // 生成対象コンポーネントおよびその依存コンポーネントが使用するコンテナを、
 // 「元のコンテナ」と「上書き用コンテナ」を合成したコンテナに変更

 overrideContainer(root);

 // コンポーネントを生成して返す
 return componentClass.cast(def.getComponent());
}

private void overrideContainer(S2Container root) {
 System.err.println(root.getNamespace());
 final S2Container newContainer = new S2ContainerImpl();
 newContainer.include(containerForOverride);
 newContainer.include(root);
 final List<ComponentDef> defs = Lists.newArrayList();
 for (int i = 0; i < root.getComponentDefSize(); i++)
  defs.add(root.getComponentDef(i));
 for (ComponentDef cd : defs) {
  cd.setContainer(newContainer);
  System.err.println(cd.getComponentName());
 }
 final List<S2Container> children = Lists.newArrayList();
 for (int i = 0; i < root.getChildSize(); i++)
  children.add(root.getChild(i));
 for (S2Container c : children)
  overrideContainer(c);
}
@Test
public void test() {
 final HogeDao mocked=mockAndRegister(HogeDao.class);
 final HogeLogic target=getComponent(HogeLogic.class);

 when(mocked).doSomething().thenReturn(0);

 assertThat(target.doSomething(), is(0));
 // ...
}


*1:詳細については、生成対象クラスのコンストラクタにブレークポイント張ってデバッガでスタックトレース眺めるとわかりやすいです