首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

EasyMock 測試 JDBC 存儲過程實戰:CallableStatement 模擬、記錄-回放模式、異常驗證完整指南

     編輯:Java 相關知識

測試場景與挑戰

專案需使用 JDBC 呼叫 Oracle 存儲過程 LUCK_LOAD_COMMON(?, ?),過往經驗多為 Hibernate/Spring 測試 (框架內部正確性無需驗證)。JDBC 直接操作資料庫連線、預存程序呼叫、參數綁定、結果集處理,**EasyMock 記錄-回放模式**可精確驗證呼叫序列與參數。

被測試代碼 (DAO 方法)

public String getNumber() {
    CallableStatement cstmt = null;
    try {
        cstmt = _conn.prepareCall("{call LUCK_LOAD_COMMON(?,?)}");
        cstmt.setString(1, "1");
        cstmt.registerOutParameter(2, java.sql.Types.VARCHAR);
        cstmt.executeUpdate();
        return cstmt.getString(2);
    } catch (Exception e) {
        GxDebug.logException(e);
        e.printStackTrace();
        return null;
    } finally {
        if (cstmt != null) try { cstmt.close(); } catch (Exception ignore) {}
    }
}

EasyMock 核心概念:記錄-回放雙階段

  1. 記錄階段:在 Mock 控制器上設定預期呼叫序列、參數匹配、返回值/拋出異常
  2. 回放階段:切換至回放模式,執行被測代碼,EasyMock 自動驗證實際呼叫與預期是否一致

完整測試代碼實戰


import static org.easymock.EasyMock.*;
import org.easymock.classextension.MockControl;  // 類別擴充支援介面/類別 Mock
import java.sql.*;

public class JdbcDaoTest {
    private MockControl conControl;
    private MockControl cstmtControl;
    private Connection conn;
    private CallableStatement cstmt;
    private JdbcDao dao;  // 被測 DAO,構造器注入 Connection

    @Before
    public void setUp() {
        // 建立 Mock 控制器
        conControl = MockControl.createControl(Connection.class);
        cstmtControl = MockControl.createControl(CallableStatement.class);
        
        // 獲取 Mock 物件
        conn = (Connection) conControl.getMock();
        cstmt = (CallableStatement) cstmtControl.getMock();
        
        // DAO 注入 Mock Connection
        dao = new JdbcDao(conn);
    }

    @Test
    public void testGetNumber_Success() {
        // ========== 記錄階段 ==========
        // 1. Connection.prepareCall() 返回 Mock CallableStatement
        conn.prepareCall("{call LUCK_LOAD_COMMON(?,?)}");
        conControl.setReturnValue(cstmt);
        
        // 2. CallableStatement 參數綁定序列
        cstmt.setString(1, "1");
        cstmt.registerOutParameter(2, Types.VARCHAR);
        
        // 3. 執行更新 (無返回值驗證)
        cstmt.executeUpdate();
        cstmtControl.setReturnValue(1);  // executeUpdate 返回影響行數
        
        // 4. 獲取輸出參數
        cstmt.getString(2);
        cstmtControl.setReturnValue("5,4,3");  // 模擬存儲過程返回值
        
        // 5. 關閉資源
        cstmt.close();
        
        // 切換至回放模式 (順序重要:先子物件後父物件)
        cstmtControl.replay();
        conControl.replay();
        
        // ========== 回放階段 ==========
        String result = dao.getNumber();
        
        // ========== 驗證 ==========
        assertEquals("5,4,3", result);
        conControl.verify();
        cstmtControl.verify();
    }

    @Test
    public void testGetNumber_SQL_String_Mismatch() {
        // 記錄階段:預期正確 SQL
        conn.prepareCall("{call LUCK_LOAD_COMMON(?,?)}");
        conControl.setReturnValue(cstmt);
        conControl.replay();
        
        // 回放階段:被測代碼若 SQL 字串不同 (如多一個參數)
        // dao.getNumber() 內部會呼叫 prepareCall("{call LUCK_LOAD_COMMON(?,?,?)}")
        
        // 驗證階段:EasyMock 拋出 AssertionFailedError
        // Unexpected method call prepareCall("{call LUCK_LOAD_COMMON(?,?)}"):
        // prepareCall("{call LUCK_LOAD_COMMON(?,?)}"): expected: 0, actual: 1
        // prepareCall("{call LUCK_LOAD_COMMON(?,?,?)}"): expected: 1, actual: 0
    }
}

關鍵技術細節解析

技術點說明最佳實踐
MockControl vs EasyMock.createMock()MockControl 提供更精細控制 (replay/verify/reset 分離)複雜序列驗證用 MockControl,簡單場景用 createMock()
介面 vs 類別 MockJDBC API 為介面 (Connection, CallableStatement),標準 EasyMock 即可若需 Mock 具體類別 (如 OraclePreparedStatement),需 EasyMock ClassExtension
參數匹配精度預設 equals() 比較,SQL 字串必須完全一致 (含空格、大小寫)使用 eq()、anyString()、aryEq() 等匹配器放寬/收緊條件
void 方法處理cstmt.close() 無返回值,記錄階段直接呼叫即可無需 setReturnValue,但需在記錄序列中包含
異常模擬cstmtControl.setThrowable(new SQLException("DB down"))測試 DAO 異常處理邏輯 (try-catch-finally 資源釋放)
驗證順序先 verify 子物件 (cstmt),再 verify 父物件 (conn)符合依賴關係,避免遺漏驗證

測試代碼量與維護成本分析

原文作者感嘆:「測試代碼比實現代碼還多」。這是 JDBC 程式化 API 特性導致的必然結果:

  • 每個 JDBC 呼叫 (prepareCall、setString、registerOutParameter、executeUpdate、getString、close) 都需在測試中顯式記錄
  • 參數值、返回值、異常情況全需手工指定
  • 存儲過程邏輯變更 (參數增減、類型變更) 將導致測試代碼大量修改

優化策略與替代方案

策略優點缺點適用場景
封裝 JDBC 模板類測試模板而非每個 DAO、複用 Mock 邏輯增加抽象層、可能掩蓋具體 SQL 錯誤大量相似 JDBC 操作專案
使用 Spring JdbcTemplate / SimpleJdbcCall框架封裝 JDBC 細節、測試關注業務參數引入 Spring 依賴、仍需 Mock JdbcTemplateSpring 生態專案
整合測試替代單元測試真實資料庫驗證端到端正確性、SQL 執行計畫需測試資料庫、速度慢、環境依賴關鍵業務邏輯、CI 階段
DbUnit + 真實資料庫資料集載入/驗證、狀態隔離配置複雜、維護成本高複雜資料邏輯驗證
契約測試驗證 DAO 介面契約而非實現細節無法驗證具體 SQL/參數正確性微服務介面契約驗證

總結

EasyMock 以記錄-回放模式精確驗證 JDBC 呼叫序列、參數綁定、返回值處理,能發現 SQL 字串不匹配、參數索引錯誤、資源未釋放等靜態分析難以發現的問題。但 JDBC 程式化 API 導致測試代碼冗長、脆弱、維護成本高。**建議策略**:核心/複雜存儲過程呼叫寫 EasyMock 單元測試,常規 CRUD 採用 Spring JdbcTemplate + 整合測試,建立分層測試金字塔。若專案大量使用 JDBC 直連,考慮引入 Spring JDBC 或 MyBatis 封裝,從源頭減少樣板代碼與測試負擔。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved