2009年12月30日水曜日

@Runwith(Parameterized + JMock): 解

前回、Parameterized と JMock が併用できずに困ったところで終わったが、併用する案を考えてみた。

Parameterized の使い方と JMock の使い方を知っていれば、コードの意味は自明だと思う。
@RunWith(ParameterizedJMockRunner.class)
public class ATest {
   @Parameters public static Collection<Object[]> params() {
      return Arrays.asList(new Object[][]{
            {-1, -2}, {0, 0}, {1, 2}, {2, 4}});
   }
   Mockery ctx = new JUnit4Mockery();
   int param;
   int expected;
   public ATest(int param, int expected) {
      this.param = param; 
      this.expected = expected;
   }
   @Test public void testCallB() {
      final B b = ctx.mock(B.class);
      ctx.checking(new Expectations(){{ oneOf(b).foo(expected); }});
      new A(b).callB(param);
   }
}


冒頭の RunWith で指定している ParameterizedJMockRunner は以下のようなもの。
public class ParameterizedJMockRunner extends Parameterized {
   public ParameterizedJMockRunner(Class<?> klass) throws Throwable {
      super(klass);
   }
   @Override
   protected List<Runner> getChildren() {
      List<Runner> original = super.getChildren();
      List<Runner> result = new ArrayList<Runner>(original.size());
      for (Runner each : super.getChildren()) try {
         result.add(new ChildRunner(each));
      } catch (InitializationError e) {
         throw new RuntimeException(e);
      }
      return result;
   }
}


オーバライドした getChildren() は、基底クラスの Parameterized が持っている子 Runner を、自前の Runner である ChildRunner にすり替えている。

ChildRunner は、以下のようなコードにした。
class ChildRunner extends BlockJUnit4ClassRunner {
   private Field mockeryField;
   private final int parameterSetNumber;
   private final List<Object[]> fParameterList;

   public ChildRunner(Runner runner) throws InitializationError {
      super(Util.getJavaClass(runner));
      try {
         parameterSetNumber = Util.parameterSetNumberOf(runner);
         fParameterList = Util.parameterListOf(runner);
         mockeryField = Util.findMockeryField(runner);
      } catch (Exception ex) {
         throw new RuntimeException(ex);
      }
   }
   @Override
   public Object createTest() throws Exception {
      return getTestClass().getOnlyConstructor().newInstance(computeParams());
   }
   private Object[] computeParams() throws Exception {
      return fParameterList.get(parameterSetNumber);
   }
   @Override
   protected String getName() {
      return String.format("[%s]", parameterSetNumber);
   }
   @Override
   protected String testName(final FrameworkMethod method) {
      return String.format("%s[%s]", method.getName(),
            parameterSetNumber);
   }
   @Override
   protected void validateZeroArgConstructor(List<Throwable> errors) {
      // constructor can, nay, should have args.
   }
   @Override
   protected Statement classBlock(RunNotifier notifier) {
      return childrenInvoker(notifier);
   }
   @Override
   protected Statement methodInvoker(
         final FrameworkMethod method, final Object test) {
      return new InvokeMethod(method, test) {
         @Override public void evaluate() throws Throwable {
            try {
               method.invokeExplosively(test);
               assertMockeryIsSatisfied(test);
            } catch (InvocationTargetException e) {
               //省略: Test.expected の処理
               throw e;
            }
         }
      };
   }
   private void assertMockeryIsSatisfied(Object testFixture) {
      mockeryOf(testFixture).assertIsSatisfied();
   }
   protected Mockery mockeryOf(Object test) {
      try { return (Mockery) mockeryField.get(test); }
      catch (Exception ex) { throw new RuntimeException(ex); }
   }
}
コードの内容は、JMock クラスのコードと Parameterized の 内部クラスTestClassRunnerForParameters のコードを混ぜたような感じ。methodInvoker のコードが JMock 系で、それ以外が TestClassRunnerForParameters系。

なんだかダラダラしたコードになったが、これでも例外処理とか手を抜いている。本当は Parameterized.TestClassRunnerForParameters コードを 継承したかったが、private なクラスなので駄目だった。

Util.xxx() とあるのは、ベタなリフレクションコードを別クラスに移動したもの。これは、さほど類推が難しくないだろうから、コードは割愛した。

こんな感じで JMock とParameterized を併用できる。まあ前ポストの、共通コードを別メソッドに括り出す方式に比べて、それ程優れてるとは言えない気もするが・・・

今回は pure Java で書いたが、AspectJ を使えばもっと簡潔に書けると思う。

0 件のコメント:

コメントを投稿