Cubby のテストを S2JUnit4 環境で行う

Cubby のアクションのテストを S2JUnit4 環境で行う方法を試してみました。テストコードはこんな感じになりますです。

@RunWith(Seasar2.class)
public class IndexActionTest {
    
    public TestContext ctx;
    
    public CubbyRunner cubbyRunner;
    
    @Test
    public void index() throws Exception{
        ActionResult actualResult = cubbyRunner.run("/");
        CubbyAssert.assertPathEquals(Forward.class, "index.jsp", actualResult);
    }
}

Eclipse で Run/Debug 設定などいじらなくても、メソッド単位の実行が出来て良い感じです。(Run/Debug 設定をクラス毎に JUnit3 のランナーに変える以外に方法を知らなかったので。。。)

書いたコード

書いたのは三つ。まずインターフェース。CubbyTestCase#processAction に該当する所。

public interface CubbyRunner {

    public ActionResult run(String path) throws Exception;

}

次に実装クラス。ほぼ CubbyTestCase ですが、request などをメソッド呼び出ししている部分を Injection されるように変更。(元は Cubby 1.1.0 の TestCase)

public class CubbyRunnerImpl implements CubbyRunner {

    @Binding
    private Router router;

    @Binding
    private ActionProcessor actionProcessor;

    @Binding
    private MockHttpServletRequest request;

    @Binding
    private MockHttpServletResponse response;

    @Binding
    private MockServletContext application;

    public ActionResult run(String originalPath) throws Exception {

        final MockHttpServletRequest request = this.request;
        setServletPath(request, originalPath);
        final MockHttpServletResponse response = this.response;
        routing(request, response);
        setupThreadContext();
        final ActionResultWrapper actionResultWrapper = actionProcessor.process(request,
                                                                                response);
        if (actionResultWrapper == null) {
            return null;
        }
        return actionResultWrapper.getActionResult();
    }

    protected String routing(final MockHttpServletRequest request,
                             final MockHttpServletResponse response) {

        final InternalForwardInfo internalForwardInfo = router.routing(request,
                                                                       response);
        if (internalForwardInfo == null) {
            throw new RuntimeException(request.getServletPath()
                    + " could not mapping to action");
        }
        final String internalForwardPath = internalForwardInfo.getInternalForwardPath();
        final MockHttpServletRequest internalForwardRequest = this.application.createRequest(internalForwardPath);
        request.setAttribute(ATTR_ROUTINGS,
                             internalForwardInfo.getOnSubmitRoutings());
        request.setAttribute("javax.servlet.forward.request_uri",
                             request.getRequestURI());
        request.setAttribute("javax.servlet.forward.context_path",
                             request.getContextPath());
        request.setAttribute("javax.servlet.forward.servlet_path",
                             request.getServletPath());
        request.setAttribute("javax.servlet.forward.path_info",
                             request.getPathInfo());
        request.setAttribute("javax.servlet.forward.query_string",
                             request.getQueryString());
        final String servletPath = internalForwardRequest.getServletPath();
        setServletPath(request, servletPath);
        request.setQueryString(internalForwardRequest.getQueryString());
        if (StringUtil.isNotBlank(internalForwardRequest.getQueryString())) {
            final Map<String, List<String>> pathParameters = parseQueryString(internalForwardRequest.getQueryString());
            for (final Entry<String, List<String>> entry : pathParameters.entrySet()) {
                final String name = entry.getKey();
                for (final String value : entry.getValue()) {
                    request.addParameter(name, value);
                }
            }
        }
        return internalForwardPath;
    }

    private static void setServletPath(final MockHttpServletRequest request,
                                       final String servletPath) {
        final Field servletPathField = ClassUtil.getDeclaredField(request.getClass(),
                                                                  "servletPath");
        servletPathField.setAccessible(true);
        try {
            servletPathField.set(request, servletPath);
        } catch (final Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    protected void setupThreadContext() {
        ThreadContext.setRequest(this.request);
    }

    private Map<String, List<String>> parseQueryString(final String queryString) {
        final Map<String, List<String>> params = new HashMap<String, List<String>>();
        final String[] tokens = queryString.split("&");
        for (final String token : tokens) {
            final String[] param = token.split("=");
            final String name = param[0];
            final String value = param[1];
            final List<String> values;
            if (params.containsKey(name)) {
                values = params.get(name);
            } else {
                values = new ArrayList<String>();
                params.put(name, values);
            }
            values.add(value);
        }
        return params;
    }
}

CubbyRunner に入れてもよいのですが、何となく流儀にしたがって CubbyAssert 。

public class CubbyAssert extends S2Assert {

    public static void assertPathEquals(
                                        final Class<? extends ActionResult> resultClass,
                                        final String expectedPath,
                                        final ActionResult actualResult) {
        assertEquals("ActionResultの型をチェック",
                     resultClass,
                     actualResult.getClass());

        if (actualResult instanceof Forward) {
            assertEquals("パスのチェック",
                         expectedPath,
                         ((Forward) actualResult).getPath());
        } else if (actualResult instanceof Redirect) {
            assertEquals("パスのチェック",
                         expectedPath,
                         ((Redirect) actualResult).getPath());
        }
    }
}

設定周り

やったのは、s2junit4-cubby.dicon の作成と s2junit4.dicon への設定の追加。

まず、s2junit4-cubby.dicon 。CubbyRunner のコンポーネント設定と、Router と ActionProcessor を Injection するため、cubby.dicon と routing.dicon を include 。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
  "http://www.seasar.org/dtd/components24.dtd">
<components namespace="s2junit4-cubby">

	<include path="cubby.dicon" />
	<include path="routing.dicon" />

	<component class="unit.CubbyRunnerImpl" />

</components>

s2junit4.dicon は ここ のを少しカスタマイズ。ConfigFileIncluderImpl に app.dicon と上記の s2juni4-cubby.dicon を追加。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
  "http://www.seasar.org/dtd/components24.dtd">
<components namespace="s2junit4">

	<component name="context"
		class="org.seasar.framework.unit.impl.InternalTestContextImpl">
		<property name="ejb3Enabled">false</property>
	</component>

	<component class="org.seasar.framework.unit.impl.DataAccessorImpl" />

	<component
		class="org.seasar.framework.unit.impl.ConfigFileIncluderImpl">
		<initMethod name="addConfigFile">
			<arg>"app.dicon"</arg>
		</initMethod>
		<initMethod name="addConfigFile">
			<arg>"s2junit4-cubby.dicon"</arg>
		</initMethod>
		<initMethod name="addConfigFile">
			<arg>context.testClassShortName + ".dicon"</arg>
		</initMethod>
	</component>
:

実行コードはほとんど変えず、クラスの乗せ換えと dicon の設定だけでいけました。ぼつぼつこっちに置き換えてみて問題ないか確かめよう。