SpringBoot Repository Integrated Test with Real DB
最初版本:直接用整個 Spring Boot app 起測試
一開始最直覺的寫法,是把它當成一般 integration test 來寫,大概像這樣:
1
2
3
4
5
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("dbtest")
class FtctlDepartRepositoryIT {
...
}
這種寫法背後的想法很簡單:
想連真的 DB,
→ repository 需要 Spring 管理
→ 那就直接讓 Spring Boot 整個起來
這個版本會用到的 import,大概是這些
1
2
3
4
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
如果 class 裡還有 assertion,通常還會有:
1
import static org.assertj.core.api.Assertions.assertThat;
為什麼這個版本有問題?
這個版本的問題不是「不能連 DB」,而是 載太多東西。
因為 @SpringBootTest 幾乎等於:
- 把整個 application context 拉起來
- 掃 config
- 掃 security
- 掃 repository
- 掃 JPA 相關設定
- 掃一堆這支 test 根本不需要的 bean
所以雖然我們要測的只有 FtctlDepartRepository
- SQL
- 它對 DB 的查詢結果
但實際上被一起啟動的卻是整包應用程式。
這時候實際碰到的錯誤類型 後來會冒出一些和目標無關的錯,例如:
- HttpSecurity 相關錯誤
- No qualifying bean
- Not a managed type
- 其他 repository / JPA entity 的錯
這些錯的共同點是:
問題不是 FtctlDepartRepository 本身,而是 test context 太大。
第二步:不再載整個 app,改成縮成「只載某些 auto-config」
發現 @SpringBootTest 太寬之後,下一個很自然的想法是:
那我不要整包 application,我只載和 DB 有關的 auto-config 就好。
所以這一步的方向,是把 FtctlDepartRepositoryIT 改成:
- 不掃整個 app
- 不碰 security
- 不碰多餘的 repository
- 只保留 DB 存取需要的設定
- 這一步的想法,通常會長這樣
想留下來的通常是:
- DataSource
- JdbcTemplate
- transaction manager
- SQL initialization 也就是把測試上下文縮成「資料庫 access 專用」。
這種版本會多出哪類 import 大概會變成這種方向:
1
2
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.context.annotation.Import;
然後搭配某些 Boot 的 auto-config class。
雖然已經比整包 @SpringBootTest 小很多了, 後來又發現一個問題:
test 開始依賴 Spring Boot 內部的 auto-config class 名稱與位置。
也就是說,test 會開始綁死在:
- 某些 auto-config class 到底在哪個 package
- 現在這個 Spring Boot 版本到底怎麼切
- annotation 指到的 class 名稱是不是完全正確
結果就容易出現這類錯:
- ClassNotFoundException
- TypeNotPresentException
- 某個 AutoConfiguration 類別根本不在你以為的位置
所以這一步的問題不是「縮小失敗」,而是:
它雖然比整包 app 小,但 still 太依賴 Spring Boot 內部結構。
第三步:再往下縮,不靠 auto-config 類名,直接在 test 裡自己定義需要的 bean
真正需要的只有這幾樣:
- DataSource
- JdbcTemplate
- FtctlDepartRepository
最好的方式不是再猜 Spring Boot 要怎麼 auto-config,
而是:
直接在 FtctlDepartRepositoryIT 裡,把這幾個 bean 自己定義出來。
最後穩定版本的核心寫法,不只是「概念上」自己補 DataSource,
而是連測試專用 context 都在 TestConfig 裡明確定義出來。
實際可用、之後也能直接照抄的版本,大概像這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@SpringBootTest(
classes = FtctlDepartRepositoryIT.TestConfig.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE
)
@ActiveProfiles("dbtest")
@EnabledIfEnvironmentVariable(named = "DBTEST_PERSON_SN", matches = ".+")
class FtctlDepartRepositoryIT {
@SpringBootConfiguration
@Import(FtctlDepartRepository.class)
static class TestConfig {
@Bean(destroyMethod = "close")
HikariDataSource dataSource(
@Value("${spring.datasource.url}") String url,
@Value("${spring.datasource.username}") String username,
@Value("${spring.datasource.password}") String password,
@Value("${spring.datasource.driver-class-name}") String driverClassName
) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setJdbcUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setReadOnly(true);
return dataSource;
}
@Bean
JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
@Autowired
private FtctlDepartRepository repository;
@Value("${dbtest.person-sn:}")
private String personSn;
@Test
void findLeaderUnits_executesQueryAndMapsRows() {
assertThat(personSn)
.as("Set DBTEST_PERSON_SN before running this integration test.")
.isNotBlank();
List<DepartLeaderUnit> units = repository.findLeaderUnits(personSn);
assertThat(units).isNotNull();
assertThat(units)
.allSatisfy(unit -> {
assertThat(unit.dpname()).isNotBlank();
assertThat(unit.dpdegree()).isNotBlank();
});
}
}
負責做的事有幾個:
- 用 @SpringBootConfiguration 告訴 Spring Boot:測試要用這個 inner class 當作設定入口
- 用 @Import(FtctlDepartRepository.class) 明確只載入這次要測的 repository
- 手動建立 DataSource,直接吃 spring.datasource.* 設定
- 用 @Bean(destroyMethod = “close”) 確保 HikariDataSource 用完會正常釋放
- 額外補一個 JdbcTemplate bean,讓 repository 需要的依賴完整成立
- 用 setReadOnly(true) 表達這支 test 是查詢用途,不希望碰寫入
最後比較重要的 import :
1
2
3
4
5
6
7
8
9
10
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
以及測試本身常見的:
1
2
3
4
5
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
只依賴:
- 一個 DataSource
- 一個 JdbcTemplate
- 一個 FtctlDepartRepository
- 一個 dbtest profile
它不再偷偷依賴:
- 整個 application context
- security config
- 其他 repository
- JPA entity scan
- Spring Boot 內部 auto-config class 名稱
所以這時候如果 test 失敗,原因通常就會真的接近目標本身:
- DB 連線有問題
- SQL 有問題
- schema / 權限有問題
- person_sn 對不到資料 這才是我們真正想要的 integration test。
整個演進過程,最簡單的版本 如果只用最短的方式描述,整個 FtctlDepartRepositoryIT 的演進就是這樣:
一開始用 @SpringBootTest 是太多。
後來縮成只載 auto-config,方向對了,但還不夠。
最後改成 test 自己提供 DataSource 和 JdbcTemplate,
才真正把測試控制在 FtctlDepartRepository 需要的範圍內。