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 了。