-テストランナー

http://d.hatena.ne.jp/masataka_k/20060528
JUnit4ではテストの実行方法について詳細に定義できる仕組みがあります。これは過去、J2EE勉強会に参加した際に、中村さんの説明を聞きながら学んだところです。その後形になって、S2JUnit4に結実しています。
http://d.hatena.ne.jp/taedium/20060605
でだ。サーブレットのテストについて、まいったなーとかめんどくせーなとか思ったことはありませんか?いや普通は思うでしょう。モックつくるのもめんどくせーし、そのモックを信頼できたものなのだろうかという気持ち悪さがあるかもしれない(たいていはモックオブジェクトで問題なく、要件をしっかり満たすとは思いますが、ここは気分の話ね)。そんなとき、テストランナー自作ですよ。

package org.ashikunep.yukara.testtools;
import org.junit.internal.runners.InitializationError;
import org.junit.internal.runners.TestClassRunner;
import org.junit.runner.notification.RunNotifier;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.ServletHolder;
public class ServerSide extends TestClassRunner {
  public ServerSide(Class<?> testClass) throws InitializationError {
    super(testClass, new InternalServerSideTestRunner(testClass));
  }
  @Override
  public void run(final RunNotifier notifier) {
    Server server = new Server(8080);
    Context context = new Context(server, "/", Context.SESSIONS);
    InternalServerSideTestRunner runner =
      (InternalServerSideTestRunner)fEnclosedRunner;
    context.addServlet(new ServletHolder(runner.getServlet()), "/*");
    try {
      server.start();
      super.run(notifier);
    } catch(Exception e) {
      e.printStackTrace();
    } finally {
      if(server.isStarted()) {
        try { 
          server.stop();
        } catch(Exception e) {
          e.printStackTrace();
        }
      }
    }
  }
}
package org.ashikunep.yukara.testtools;
import java.lang.reflect.Method;
import org.junit.internal.runners.TestClassMethodsRunner;
import org.junit.runner.notification.RunNotifier;
class InternalServerSideTestRunner extends TestClassMethodsRunner {
  private InternalTestInvokerServlet _servlet;
  public InternalServerSideTestRunner(Class<?> testClass) {
    super(testClass);
    _servlet = new InternalTestInvokerServlet(testClass);
  }
  public InternalTestInvokerServlet getServlet() {
    return _servlet;
  }
  protected Object createTest() throws Exception {
    Object test = super.createTest();
    _servlet.setTestInstance(test);
    return test;
  }
  protected void invokeTestMethod(Method method, RunNotifier notifier) {
    _servlet.setTestMethod(method);
    super.invokeTestMethod(method, notifier);
  }
}
package org.ashikunep.yukara.testtools;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
class InternalTestInvokerServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;
  private Map<String, Method> _testMethods;
  private Object _testInstance;
  private Method _testMethod;
  public InternalTestInvokerServlet(Class<?> testClass) {
    _testMethods = new HashMap<String, Method>();
    for(Method method: testClass.getMethods()) {
      if(method.isAnnotationPresent(ServerSideTest.class)) {
        ServerSideTest annotation =
          method.getAnnotation(ServerSideTest.class);
        _testMethods.put(annotation.value(), method);
      }
    }
  }
  public void setTestInstance(Object testInstance) {
    _testInstance = testInstance;
  }
  public void setTestMethod(Method testMethod) {
    _testMethod = testMethod;
  }
  protected void service(HttpServletRequest req,
      HttpServletResponse res) throws ServletException, IOException {
    if(_testInstance == null || _testMethod == null) {
      throw new IllegalStateException();
    }
    String testName = _testMethod.getName();
    Method method = _testMethods.get(testName);
    if(method == null) {
      throw new RuntimeException(
          "The server-side test not found for \"" + testName + "\".");
    }
    Class<?>[] types = method.getParameterTypes();
    Object[] args = new Object[types.length];
    for(int i = 0; i < types.length; i++) {
      if(types[i].isAssignableFrom(HttpServletRequest.class)) {
        args[i] = req;
        continue;
      }
      if(types[i].isAssignableFrom(HttpServletResponse.class)) {
        args[i] = res;
        continue;
      }
      if(types[i].isAssignableFrom(ServletConfig.class)) {
        args[i] = getServletConfig();
        continue;
      }
      if(types[i].isAssignableFrom(ServletContext.class)) {
        args[i] = getServletContext();
        continue;
      }
      throw new RuntimeException("Unknown type parameter, \"" +
          types[i].getSimpleName() + "\"." );
    }
    try {
      method.invoke(_testInstance, args);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    } catch (InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }
}

JUnit4の秀逸なところはテストランナーこそがテスト実行の挙動を決めてるところで、それを作りこめばとても柔軟なテスト実行ができることかと思います。今回はJetty6を内部起動して、本物のサーブレットAPIのコンテキストを得て、それをテストクラス呼び出しに利用する仕組みを作れました。

package org.ashikunep.yukara.scratchpad;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.ashikunep.yukara.testtools.DumpTool;
import org.ashikunep.yukara.testtools.ServerSide;
import org.ashikunep.yukara.testtools.ServerSideTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tigris.subversion.javahl.ClientException;
import org.tigris.subversion.javahl.Revision;
import org.tigris.subversion.javahl.SVNClient;
@RunWith(ServerSide.class)
public class TestToolsSample {
  @Test
  public void getLogMessage() throws ClientException {
    SVNClient client = new SVNClient();
    client.logMessages("http://localhost:8080/svn/ikushipe",
        Revision.getInstance(1), Revision.HEAD);
  }
  @ServerSideTest("getLogMessage")
  public void getLogMessageServerSide(HttpServletRequest req,
      HttpServletResponse res) throws IOException {
    DumpTool.dumpRequest(req, System.out);
    res.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
  }
}

上記のテストメソッド「getLogMessage」ではJavaHLを用いてWebDAV通信をします。そのサーバサイドの受けはJetty6-InternalTestInvokerServletと来て、getLogMessageServerSide()メソッドが呼び出されます。その際に引数に渡されるHttpServletRequestやHttpServletResponseは「本物」のオブジェクトです。基本的なアイディアをCactusからもらって、JUnit4のテストランナーで実装した例でした。