採用標準「DAO + Helper + Action + Struts」分層架構的項目,在測試階段暴露出 Action 層難以單元測試的痛點。本文複盤問題根因,提出分層解耦、依賴注入、測試驅動設計的改進方案,構建無容器依賴的高可測試系統。
| 層級 | 測試現狀 | 問題 |
| DAO | JUnit + Eclipse 完美運行 | 無 |
| Helper (Service) | 同 DAO 方式測試通過 | 無 |
| Action (Controller) | StrutsTestCase (Cactus/Mock) 難以模擬 mapping/form/request/response,工具支持不足,最終放棄單測 | 強耦合 Servlet API、Struts 基礎類,無法脫容器運行 |
2. 根因分析:Action 執行方法過於臃腫
// 典型 Struts Action 執行方法
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
// 1. 參數提取/校驗
// 2. 調用 Helper 業務邏輯
// 3. 結果封裝、設置 request/session 屬性
// 4. 返回 mapping.findForward(...)
}
問題:單一方法承擔參數處理、業務調用、視圖跳轉、Servlet API 依賴,導致:
- 無法在無容器環境實例化
ActionMapping、HttpServletRequest 等。
- Mock 對象構建成本高,行為模擬不完整(如
request.getRequestDispatcher() 鏈式調用)。
- 業務邏輯與框架代碼耦合,違反單一職責原則。
3. 改進方案:最小依賴原則 + 依賴倒置
3.1 拆分 Action 方法,剝離框架依賴
public class UserAction extends Action {
private UserService userService; // 注入接口,而非具體實現
// 入口方法:僅做框架適配,參數轉換,調用核心業務方法
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
UserQueryDTO dto = extractAndValidate(form, request); // 提取為純數據對象
UserResult result = queryUserList(dto); // 核心業務邏輯,無框架依賴
prepareView(request, result); // 視圖數據準備
return mapping.findForward("success");
}
// 核心業務方法:參數為 POJO,返回 POJO,無任何 Servlet/Struts 依賴
public UserResult queryUserList(UserQueryDTO dto) {
// 業務邏輯:調用 Service、組裝數據、異常處理
return userService.query(dto);
}
// 視圖準備:可選拆分,便於測試驗證 request 屬性設置
protected void prepareView(HttpServletRequest request, UserResult result) {
request.setAttribute("userList", result.getList());
request.setAttribute("total", result.getTotal());
}
// 參數提取校驗:可測試,返回 DTO
protected UserQueryDTO extractAndValidate(ActionForm form, HttpServletRequest request) { ... }
}
3.2 為測試暴露可控接口
- 核心業務方法
public/protected,參數/返回值為 POJO/DTO。
- 依賴(Service、DAO)通過構造器/Setter 注入,測試時替換為 Mock/Stub。
- 避免在業務方法中直接使用
request.getSession()、ServletContext 等,改為傳入所需上下文對象。
3.3 測試代碼示例
@Test
public void testQueryUserList() {
// Given
UserService mockService = Mockito.mock(UserService.class);
UserResult expected = new UserResult(Arrays.asList(new User("Alice")), 1);
when(mockService.query(any(UserQueryDTO.class))).thenReturn(expected);
UserAction action = new UserAction();
action.setUserService(mockService); // 依賴注入
// When
UserQueryDTO dto = new UserQueryDTO("keyword", 1, 20);
UserResult actual = action.queryUserList(dto);
// Then
assertEquals(expected, actual);
verify(mockService).query(dto);
}
4. 架構層面的測試友好設計原則
- 分層純函數化:Controller/Service/DAO 核心邏輯盡量寫成「輸入 POJO → 處理 → 輸出 POJO」的純函數形式。
- 依賴接口而非實現:所有層間依賴面向接口,構造器注入,便於測試替換。
- 框架適配層極薄:Struts/Spring MVC Controller 僅做參數綁定、調用服務、視圖解析,零業務邏輯。
- 領域模型貧血 → 充血:DTO 僅攜帶數據,領域對象封裝業務行為,便於單獨測試領域邏輯。
- 基礎設施抽象:緩存、消息、外部 API、定時任務等定義接口,業務代碼依賴接口,測試用 In-Memory 實現。
5. 工具鏈升級建議
- 放棄 StrutsTestCase/Cactus,遷移至 Spring Test + MockMvc(整合測試)+ JUnit 5 + Mockito(單元測試)。
- 使用 Testcontainers 管理測試數據庫(MySQL/PostgreSQL),替代 H2 避免方言差異。
- 集成 JaCoCo 代碼覆蓋率,CI 流程強制門檻(如行覆蓋 ≥ 80%)。
6. 總結
「可測試性」是架構質量的核心指標。通過 拆分臃腫方法、剝離框架依賴、面向接口編程、依賴注入,將業務邏輯從 Servlet 容器中解放出來,實現真正的單元測試。這不僅提升代碼質量,更倒逼出清晰、低耦合、高內聚的架構設計——這才是測試驅動開發(TDD)帶來的最大紅利。
如果你的代碼難以測試,那麼架構一定有問題。為了測試而重構,最終受益的是系統的可維護性與演進能力。
- 上一篇文章: 致頹廢論者:35 歲不是程序員的終點,而是轉型的起點
- 下一篇文章: Java Thread API 深度解讀:從源碼視角理解線程核心機制
Copyright ©
程式碼之家 All Rights Reserved