在過去的開發經驗中,我常發現許多開發者不寫測試代碼,理由往往是「難以編寫涵蓋多模組的測試用例」。其實,這多半是因為缺乏可控的測試技巧,或是專案進度壓力下無暇深入研究。忽略整合測試的後果是軟體品質下降、Bug 頻出、客戶信心動搖。本文將分享實戰經驗,揭開 Spring 整合測試的神祕面紗。
假設有一個 Spring 專案整合了銀行 Web Service。撰寫測試時會面臨三大困境:
單元測試時我們可輕易 Mock 依賴,但整合測試需將多個元件納入 Spring 容器管理。Spring 雖提供優秀的測試框架支援從配置檔注入 Bean,但外部服務仍需自行模擬實作。
許多人直覺會在 setUp 階段重新注入(覆蓋)Spring 注入的 Bean。警告:這會破壞容器原本行為,無法保證真實環境下表現一致。
我們不需要先寫 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>
建立測試專用配置 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,而非生產環境的實作。
若想完全移除額外 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)。