DB에 접속하는 통합 테스트는 보통 초기화메서드에서 테스트 데이터를 준비. 테스트 메서드가 하나씩 실행되면 그때마다 DB 데이터가 수정되므로 그다음 테스트 메서드를 일관성 있게 실행하려면 부득이 DB를 정리해야 함
스프링에서는 테스트 메서드마다 트랜잭션을 생성 및 롤백할 수 있어서 어느 테스트 메서드가 변경한 데이터가 그다음 테스트 메서드에 아무런 영향을 끼치지 않음
개발자가 DB 정리 코드를 직접 작성해야 할 부담이 없음
테서트 컨텍스트 프레임워크는 트랜잭션 관리와 연관된 테스트 실행 리스너인 TransactionalTestExecutionListener를 제공
다른 리스너를 지정하지 않는 한 이 실행 리스너는 테스트 컨텍스트 관리자에 기본 등록
TransactionalTestExecutionListener는 클래스/메서드 레벨에 적용한 @Transactional을 감지해 자동으로 메서드에 트랜잭션을 걸음
테스트 클래스 메서드에 트랜잭션을 걸려고 상속하는 테스트 컨텍스트 지원 클래스
테스트 컨텍스트 관리자와 함께 작동하며 클래스 레벨에 @Transactional 기능을 활성화
트랜잭션 관리자도 빈 구성 파일에 등록
| 테스트 프레임워크 | 테스트 컨텍스트 지원 클래스 |
|---|---|
| JUnit | AbstractTransactionalJUnit4SpringContextTests |
| TestNG | AbstractTransactionalTestNGSpringContextTests |
|:---|:---|
|DependencyInjectionTestExecutionListener|애플리케이션 컨텍스트를 비롯한 모든 의존체를 테스트에 주입|
|DirtiesContextTestExecutionListener, DirtiesContextBeforeModesTestExecutionListener|@DirtiesContext 처리를 담당하며 필요 시 애플리케이션 컨텍스트를 다시 로드|
|TransactionalTestExecutionListener|테스트 케이스의 @Transactional을 처리하며 테스트 끝부분에서 롤백을 수행|
|SqlScriptsTestExecutionListener|@Sql을 붙인 테스트를 감지해서 테스트를 시작하기 전에 주어진 SQL을 실행|
|ServletTestExecutionListener|@WebAppConfiguration이 발견되면 웹 애플리케이션 컨텍스트를 로드|
테스트 컨텍스트 지원 클래스는 DependencyInjectionTestExecutionListener, DirtiesContextTestExecutionListener, TransactionalTestExecutionListener, SqlScriptsTestExecutionListener 네 TestExecutionListener 구현체를 활성화
JUnit과 TestNG에서는 클래스/메서드 레벨에 @Transactional을 붙여 테스트 컨텍스트 지원 클래스를 상속하지 않고도 테스트 메서드에 트랜잭션을 걸 수 있음
그러나 테스트 컨텍스트 관리자와 연동하려면 JUnit 테스트는 SpringRunner 테스트 실행기로, TestNG 테스트는 수동으로 실행
RDBMS를 사용하는 은행 시스템에서 계정 정보를 저장
트랜잭션이 지원되고 JDBC와 호환되는 DB 엔진을 선택
ACCOUNT 테이블 생성
CREATE TABLE ACCOUNT (
ACCOUNT_NO VARCHAR(10) NOT NULL,
BALANCE DOUBLE NOT NULL,
PRIMARY KEY (ACCOUNT_NO)
);
JDBC를 사용해 DB를 액세스하는 DAO 구현체
public class JdbcAccountDao extends JdbcDaoSupport implements AccountDao {
public void createAccount(Account account) {
String sql = "INSERT INTO ACCOUNT (ACCOUNT_NO, BALANCE) VALUES (?, ?)";
getJdbcTemplate().update(
sql, account.getAccountNo(), account.getBalance());
}
public void updateAccount(Account account) {
String sql = "UPDATE ACCOUNT SET BALANCE = ? WHERE ACCOUNT_NO = ?";
getJdbcTemplate().update(
sql, account.getBalance(), account.getAccountNo());
}
public void removeAccount(Account account) {
String sql = "DELETE FROM ACCOUNT WHERE ACCOUNT_NO = ?";
getJdbcTemplate().update(sql, account.getAccountNo());
}
public Account findAccount(String accountNo) {
String sql = "SELECT BALANCE FROM ACCOUNT WHERE ACCOUNT_NO = ?";
double balance = getJdbcTemplate().queryForObject(
sql, Double.class, accountNo);
return new Account(accountNo, balance);
}
}
구성 파일 설정
@Configuration
@ComponentScan("com.apress.springrecipes.bank")
public class BankConfiguration {
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Configuration
@Profile("!in-mem")
@PropertySource("classpath:/application.properties")
public static class JdbcBankConfiguration {
private final Environment env;
public JdbcBankConfiguration(Environment env) {
this.env = env;
}
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.username"));
dataSource.setPassword(env.getProperty("jdbc.password"));
return dataSource;
}
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public AccountDao accountDao(DataSource dataSource) {
JdbcAccountDao accountDao = new JdbcAccountDao();
accountDao.setDataSource(dataSource);
return accountDao;
}
}
}
테스트 컨텍스트 프레임워크에서 작성한 테스트의 클래스/메서드 레벨에 @Transactional을 붙이면 테스트 메서드에 트랜잭션이 적용
JUnit에서는 지원 클래스를 상속하지 않아도 SpringRunner를 테스트 실행기로 지정 가능
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = BankConfiguration.class)
@Transactional
public class AccountServiceJUnit4ContextTests {
private static final String TEST_ACCOUNT_NO = "1234";
@Autowired
private AccountService accountService;
@Before
public void init() {
accountService.createAccount(TEST_ACCOUNT_NO);
accountService.deposit(TEST_ACCOUNT_NO, 100);
}
...
}
테스트 클래스에 @Transactional을 붙여 그 클래스의 모든 테스트 메서드에 트랜잭션이 적용
메서드마다 개별적으로 적용 가능
기본적으로 테스트 메서드에 적용된 트랜잭션은 메서드 실행이 끝나면 무조건 롤백
이 로직을 변경하고 싶으면 클래스 레벨에만 붙이는 @TransactionConfiguration의 defaultRollback 속성을 false로 설정
클래스 레벨의 롤백 로직을 메서드별로 오버라이드하려면 해당 메서드에 Rollback(false) 선언
@Before,@After를 붙인 메서드는 테스트 메서드와 같은 트랜잭션 내에서 실행. 트랜잭션 전후에 각각 초기화 및 정리 작업이 필요한 경우@BeforeTransaction,afterTransaction을 붙임
빈 구성 파일에 트랜잭션 관리자 구성
기본적으로 PlatformTransactionManager형 빈을 사용
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
JUnit에서 테스트 프레임워크의 지원 클래스인 AbstractTransactionalJUnit4SpringContextTests를 상속하면 이미 클래스 레벨에 @Transactional이 달려 있고 SpringRunner도 상위 클래스에서 상속받기 때문에 별도의 지정 필요 없음
@ContextConfiguration(classes = BankConfiguration.class)
public class AccountServiceJUnit4ContextTests extends AbstractTransactionalJUnit4SpringContextTests {
...
}
TestNG 테스트 클래스가 AbstractTransactionalTestNGSpringContextTests를 상속하면 내부 모든 메서드에 트랜잭션이 걸림
@ContextConfiguration(classes = BankConfiguration.class)
public class AccountServiceTestNGContextTests extends AbstractTransactionalTestNGSpringContextTests {
private static final String TEST_ACCOUNT_NO = "1234";
@Autowired
private AccountService accountService;
@BeforeMethod
public void init() {
accountService.createAccount(TEST_ACCOUNT_NO);
accountService.deposit(TEST_ACCOUNT_NO, 100);
}
@Test
public void deposit() {
accountService.deposit(TEST_ACCOUNT_NO, 50);
assertEquals(accountService.getBalance(TEST_ACCOUNT_NO), 150, 0);
}
@Test
public void withDraw() {
accountService.withdraw(TEST_ACCOUNT_NO, 50);
assertEquals(accountService.getBalance(TEST_ACCOUNT_NO), 50, 0);
}
}