Spring 2.5 引入基於註解的 TestContext 框架,徹底改變 Spring 集成測試寫法。支援 JUnit 3.8/4.4、TestNG,解決傳統 JUnit 測試的四大痛點:容器重複啟動、Bean 手動獲取、資料庫現場汙染、事務環境驗證困難。
| 問題 | 原因 | 後果 |
|---|---|---|
| 容器多次初始化 | 每個測試方法建立實例、調用 setUp() | 啟動慢、資源浪費 |
| 硬編碼獲取 Bean | ctx.getBean("xxx") + 強制轉型 | 重構易碎、類型不安全 |
| 資料庫現場破壞 | 測試數據殘留、主鍵衝突 | 測試不可重複執行 |
| 同事務驗證困難 | 測試方法與業務代碼不同事務 | 無法即時查詢驗證 DB 狀態 |

DependencyInjectionTestExecutionListener:處理 @Autowired 注入DirtiesContextTestExecutionListener:處理 @DirtiesContext 重載容器TransactionalTestExecutionListener:處理事務註解、回滾public class UserService {
private UserDao userDao;
private LoginLogDao loginLogDao;
public void handleUserLogin(User user) {
user.setCredits(5 + user.getCredits());
LoginLog loginLog = new LoginLog();
loginLog.setUserId(user.getUserId());
loginLog.setIp(user.getLastIp());
loginLog.setLoginTime(user.getLastVisit());
userDao.updateLoginInfo(user);
loginLogDao.insertLoginLog(loginLog);
}
// getters/setters
}
<beans ... xmlns:tx="..." xmlns:aop="..." xmlns:context="...">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" .../>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" p:dataSource-ref="dataSource"/>
<bean id="userDao" class="com.baobaotao.dao.UserDao" p:jdbcTemplate-ref="jdbcTemplate"/>
<bean id="loginLogDao" class="com.baobaotao.dao.LoginLogDao" p:jdbcTemplate-ref="jdbcTemplate"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/>
<bean id="userService" class="com.baobaotao.service.UserService" p:userDao-ref="userDao" p:loginLogDao-ref="loginLogDao"/>
<aop:config>
<aop:pointcut id="serviceMethods" expression="within(com.baobaotao.service..*)"/>
<aop:advisor pointcut-ref="serviceMethods" advice-ref="txAdvice"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes><tx:method name="*"/></tx:attributes>
</tx:advice>
</beans>
@ContextConfiguration // 預設載入 classpath:/com/baobaotao/service/TestUserService-context.xml
public class TestUserService extends AbstractTransactionalJUnit4SpringContextTests {
@Autowired
private UserService userService;
@Autowired
private UserDao userDao;
private int userId;
@Before // 每個測試方法前執行,同事務環境
public void prepareTestData() {
// 使用 simpleJdbcTemplate (父類提供) 插入測試數據
final String sql = "insert into t_user(user_name,password) values('tom','1234')";
simpleJdbcTemplate.update(sql);
// 獲取自增主鍵
KeyHolder keyHolder = new GeneratedKeyHolder();
simpleJdbcTemplate.getJdbcOperations().update(
new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
}
}, keyHolder);
userId = keyHolder.getKey().intValue();
}
@Test
public void handleUserLogin() {
User user = userDao.getUserById(userId);
user.setLastIp("127.0.0.1");
Date now = new Date();
user.setLastVisit(now.getTime());
userService.handleUserLogin(user);
// === 業務結果驗證 ===
User newUser = userDao.getUserById(userId);
Assert.assertEquals(5, newUser.getCredits()); // 積分增加 5
Assert.assertEquals(now.getTime(), newUser.getLastVisit());
Assert.assertEquals("127.0.0.1", newUser.getLastIp());
// 驗證登錄日誌
String sql = "select count(1) from t_login_log where user_id=? and login_datetime=? and ip=?";
int logCount = simpleJdbcTemplate.queryForInt(sql, user.getUserId(), user.getLastVisit(), user.getLastIp());
Assert.assertEquals(1, logCount);
}
}
<beans ...>
<import resource="classpath:/applicationContext.xml"/>
</beans>
| 註解 | 作用 | 屬於 |
|---|---|---|
@ContextConfiguration | 指定 Spring 配置位置,支援 locations、inheritLocations | 類級 |
@Autowired | 測試類欄位/Setter 自動注入 Bean | 欄位/方法 |
@Before / @After | JUnit 4.x 生命週期,同測試方法事務 | 方法 |
@BeforeTransaction / @AfterTransaction | 事務開始前/結束後執行,適合準備/驗證全局數據 | 方法 |
@Transactional | 方法級事務控制,預設回滾 | 類/方法 |
@NotTransactional | 禁用事務 (預設父類啟用事務) | 方法 |
@Rollback(false) | 提交事務而非回滾 | 方法 |
@TransactionConfiguration | 自定義事務管理器、預設回滾策略 | 類級 |
@DirtiesContext | 標記測試汙染容器,執行後重載 ApplicationContext | 類/方法 |
simpleJdbcTemplate:自動配置的 SimpleJdbcTemplate,直接操作 DBapplicationContext:ApplicationContext 引用,可獲取任意 BeancountRowsInTable("table"):快速統計表行數deleteFromTables("t1", "t2"):批量清表executeSqlScript("path.sql", true):執行 SQL 腳本 (一行一條)@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class})
public class AbstractJUnit4SpringContextTests implements ApplicationContextAware { ... }
@TestExecutionListeners({TransactionalTestExecutionListener.class})
@Transactional
public class AbstractTransactionalJUnit4SpringContextTests extends AbstractJUnit4SpringContextTests { ... }
關鍵點:@RunWith(SpringJUnit4ClassRunner.class) 將 TestContext 無縫整合進 JUnit 4.x 運行器;測試類不需在 Spring 配置中定義,也不需 <context:annotation-config/>,監聽器由框架自動註冊。
org.springframework.test.context.junit38org.springframework.test.context.junit4org.springframework.test.context.testng@Autowired,告別 getBean() 強轉TestContext 框架讓 Spring 集成測試從「痛苦的必要惡」變為「優雅的工程實踐」,是每個 Spring 開發者必須掌握的核心技能。