17 July 2005

Struts 跟 Spring integration 的方式有三種,其中兩種方式都是用Delegate的方式讓 Action 交給 Spring 控管。這種做法可以讓 Action 享有 Spring IoC 的能力,因此 Action 的 Unit test 較為簡單,然而這種做法有個缺點:它必需在 struts-config.xml 以及 applicationContext.xml 裡各寫一次 URL。這個就頭痛了,URL 已經夠難管理了,還寫在兩個地方... 算的上是最臭的 "bad smell" 了。因此我們的專案中就採取最原始的 integration 方式:ActionSupport。ActionSupport可以直接取得 Spring 的 WebApplicationContext,呼叫 getBean("foo") 即可取得 Spring 管理的資源。這樣做法就不用動到 config 檔、也不用寫兩次 URL。

這種 integration 方式雖然簡單,但問題在於 getBean("foo") 這種做法已不算是 IoC 了,它是一種 "look-up"。Look-up的方式不似 IoC 般,可以讓我們在 test 中 inject mock object。因此如果要進行 Struts Action 的 Unit Test。我們必須 "做一些手腳",讓 StrutsTestCase 可以 Look-up Mock Object。

整個概念不難:我們替 StrutsTestCase 製做一個空的 WebApplicationContext,然後註冊協助測試的 mock Object,如此 Action 在測試其間便不會用到真正的 object ,而是 look-up 到 mock Object :
/**
 * subclass then use prepareMockWebApplicationContext() to register a mock bean
 * that used in Struts Action.
 */
public abstract class AbstractIntegrationMockStrutsTestCase extends
        MockStrutsTestCase {

    /**
     * setup MockWebApplicationContext with one mockBean, note that:
     */
    protected final void prepareMockWebApplicationContext(String beanName,
            Object mockSingleton) {
        prepareMockWebApplicationContext(new String[] { beanName },
            new Object[] { mockSingleton });
    }

    /**
     * setup MockWebApplicationContext with multiple mockBeans, note that:
     */
    protected final void prepareMockWebApplicationContext(String[] beanNames,
            Object[] mockSingletons) {
        if (beanNames.length != mockSingletons.length) {
            throw new IllegalArgumentException(
                    "size of names and mockBeans must be the same");
        }

        StaticWebApplicationContext mockCtx = new StaticWebApplicationContext();

        for (int i = 0; i < mockSingletons.length; i++) {
            mockCtx.getDefaultListableBeanFactory().registerSingleton(
                beanNames[i], mockSingletons[i]);
        }

        setWebApplicationContext(mockCtx);
        mockCtx.refresh();

    }

    private void setWebApplicationContext(
            WebApplicationContext webApplicationContext) {
        getActionServlet().getServletContext().setAttribute(
            WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
            webApplicationContext);
    }

    private StaticWebApplicationContext getWebApplicationContext() {
        return (StaticWebApplicationContext) getActionServlet()
                .getServletContext()
                .getAttribute(
                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    }

    /**
     * prepare a blank WebApplicationContext 
     */
    protected final void prepareBlankWebApplicationContext() {
        setWebApplicationContext(new StaticWebApplicationContext());
    }
}

關鍵在於 StaticWebApplicationContext,這是 Spring 設計用來幫助測試的 programmable ApplicationContext。我們可以藉由它的 getDefaultListableBeanFactory().registerSingleton(...) 手動註冊物件到裡面。當 Context 建立完成後,便可存放在 ServletContext 的 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE attribute 裡供 ActionSupport 使用。

如何使用這個 Test Case?

//This is production code:
public class CreateAccontAction extends ActionSupport
    public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response) {

        //look-up "accountService" bean
        AccountService service = (AccountService) getWebApplicaitonContext()
                                            .getBean("accountService") ;
        service.create(...) //do business logic
        return mapping.findForward("create");
    }
}

//This is test case, subclass of AbstractIntegrationMockStrutsTestCase
public class CreateAccountActionTest extends AbstractIntegrationMockStrutsTestCase {
    protected void setUp() {
        super.setUp() ;
        MockAccountService mockAccountService = new MockAccountService() ;
        prepareMockWebApplicationContext("accountService", mockAccountService);
    }
}

CreateAccountAction 繼承 ActionSupport,並且用 look-up 的方式取得 service facade "accountService"。而它的 test case 則是在測試前先註冊一個 mockAccountService。這樣就可以單獨測試 Action 本身,而不用碰觸到 business logic 了。