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