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

Spring 整合測試實戰:用 Springockito 優雅地 Mock 外部依賴

     編輯:Java 相關知識

前言:為什麼整合測試總是令人頭疼?

在過去的開發經驗中,我常發現許多開發者不寫測試代碼,理由往往是「難以編寫涵蓋多模組的測試用例」。其實,這多半是因為缺乏可控的測試技巧,或是專案進度壓力下無暇深入研究。忽略整合測試的後果是軟體品質下降、Bug 頻出、客戶信心動搖。本文將分享實戰經驗,揭開 Spring 整合測試的神祕面紗。

核心痛點:外部服務難以模擬

假設有一個 Spring 專案整合了銀行 Web Service。撰寫測試時會面臨三大困境:

  1. 每次測試觸發真實業務交易,產生實際金流成本;
  2. 高頻請求可能被銀行視為惡意攻擊導致帳號封鎖;
  3. 非生產環境測試結果不可靠,難以驗證真實行為。

單元測試時我們可輕易 Mock 依賴,但整合測試需將多個元件納入 Spring 容器管理。Spring 雖提供優秀的測試框架支援從配置檔注入 Bean,但外部服務仍需自行模擬實作。

錯誤示範:在 setUp 階段強行替換 Bean

許多人直覺會在 setUp 階段重新注入(覆蓋)Spring 注入的 Bean。警告:這會破壞容器原本行為,無法保證真實環境下表現一致。

優雅解法:讓 Spring 一開始就注入 Mock 物件

我們不需要先寫 Mock 類再手動注入,而是讓 Spring 啟動時直接注入 Mock 版本。以下以 BankService(外部服務)與 UserBalanceService(業務邏輯)為例示範。

領域模型與配置

// BankService.java
public interface BankService {
    String getBalanceByEmail(String email);
}

// BankServiceImpl.java
public class BankServiceImpl implements BankService {
    @Override
    public String getBalanceByEmail(String email) {
        throw new UnsupportedOperationException("Operation failed due to external exception");
    }
}

// UserBalanceService.java
interface UserBalanceService {
    Double getAccountBalance(String email);
}

// UserBalanceServiceImpl.java
public class UserBalanceServiceImpl implements UserBalanceService {
    @Autowired private BankService bankService;
    @Override
    public Double getAccountBalance(String email) {
        return Double.valueOf(bankService.getBalanceByEmail(email));
    }
}

生產環境配置 applicationContext.xml:

<beans ...>
    <bean id="bankService" class="...BankServiceImpl"/>
    <bean id="userBalanceService" class="...UserBalanceServiceImpl"/>
</beans>

方案一:使用 Spring Profile + Springockito XML 配置

建立測試專用配置 testApplicationContext.xml,引入生產配置並在 springTest profile 下定義 Mock Bean:

<beans ... xmlns:mockito="https://www.mockito.org/spring/mockito" ...>
    <import resource="classpath:/springtest/springockito/applicationContext.xml"/>
    <beans profile="springTest">
        <mockito:mock id="bankService" class="...BankService"/>
    </beans>
</beans>

測試類啟用 profile 並在 @Before 定義 Mock 行為:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/springtest/springockito/testApplicationContext.xml")
@ActiveProfiles(profiles = {"springTest"})
public class UserBalanceServiceImplProfileTest {
    @Autowired private UserBalanceService userBalanceService;
    @Autowired private BankService bankService;

    @Before
    public void setUp() {
        Mockito.when(bankService.getBalanceByEmail("user@bank.com"))
               .thenReturn(String.valueOf(123.45D));
    }

    @Test
    public void shouldReturnMockedBalance() {
        Double balance = userBalanceService.getAccountBalance("user@bank.com");
        assertEquals(balance, Double.valueOf(123.45D));
    }
}

測試通過,因為 Spring 注入的是 Springockito Mock 的 Bean,而非生產環境的實作。

方案二:進階優化——Springockito Annotations 註解驅動

若想完全移除額外 XML 配置,可使用 Springockito-annotations 模組,直接在測試類以註解聲明 Mock:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = SpringockitoContextLoader.class, 
                      locations = "classpath:/springtest/springockito/applicationContext.xml")
public class UserBalanceServiceImplAnnotationTest {
    @Autowired private UserBalanceService userBalanceService;
    @Autowired @ReplaceWithMock private BankService bankService; // 關鍵註解

    @Before
    public void setUp() {
        Mockito.when(bankService.getBalanceByEmail("user@bank.com"))
               .thenReturn(String.valueOf(123.45D));
    }

    @Test
    public void shouldReturnMockedBalance() {
        Double balance = userBalanceService.getAccountBalance("user@bank.com");
        assertEquals(balance, Double.valueOf(123.45D));
    }
}

優勢:直接使用生產環境 applicationContext.xml,無需額外 XML、無需修改生產配置、測試代碼乾淨解耦。

結語

Springockito-annotations 讓整合測試建立在依賴倒置基礎上,最大程度降低測試對生產代碼的侵入性,也省去維護額外 XML 的負擔。強烈建議在 Spring 整合測試中採用此方案,讓測試真正變得「簡單可靠」。

本文代碼參考自作者 GitHub,原文出處:Test Me If You Can #1 (Spring Framework)。

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