이번 issue-tracker 프로젝트를 진행하면서 테스트별로 독립된 환경을 제공할 필요가 있었습니다. 이를 위해 테스트별로 DB환경을 초기화해주면 좋겠다 싶어서 자동으로 DB환경을 초기화해주는 코드를 작성하게 되었습니다.
글을 작성하기에 앞서 Java11, Spring Boot 2.7.14, spring-boot-starter-jdbc를 사용하였음을 알립니다.
우선은 @BeforeEach
애노테이션을 통해 데이터베이스를 초기화할 생각을 하게 되었습니다.
이를 위해 매번 테이블의 모든 내용을 비워주는 기능을 구현했습니다.
@Component
public class DatabaseInitializer {
private static final String TRUNCATE_QUERY = "TRUNCATE TABLE %s";
private static final String AUTO_INCREMENT_INIT_QUERY = "ALTER TABLE %s AUTO_INCREMENT = 1";
@Autowired
private DataSource dataSource;
@Autowired
private NamedParameterJdbcTemplate jdbcTemplate;
private final List<String> tableNames = new ArrayList<>();
@PostConstruct
public void afterConstruct() {
try {
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
ResultSet tables = metaData.getTables(null, null, null, new String[] {"TABLE"});
while (tables.next()) {
String tableName = tables.getString("TABLE_NAME");
tableNames.add(tableName);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Transactional
public void truncateTables() {
for (String tableName : tableNames) {
truncateTable(tableName);
}
}
private void truncateTable(final String tableName) {
jdbcTemplate.update(String.format(TRUNCATE_QUERY, tableName), Map.of());
jdbcTemplate.update(String.format(AUTO_INCREMENT_INIT_QUERY, tableName), Map.of());
}
}
그리고 @AfterEach
애노테이션을 통해 매 테스트마다 DB의 테이블을 비워주는 truncateTables
메서드를 호출했습니다.
@Autowired
private DatabaseInitializer databaseInitializer;
@BeforeEach
void setUp() {
databaseInitializer.truncateTables();
}
간단하게 databaseInitializer 빈을 주입받고 truncateTables 메서드를 호출하는 것으로 독립된 DB환경을 제공하도록 구현했습니다.
그런데 JUnit5의 Extension을 이용하면 @AfterEach
메서드를 정의하지 않고 간단한 애노테이션을 붙이는 것만으로 같은 동작을 하게 할 수 있습니다.
JUnit5 Extension은 테스트 실행 중 특정 이벤트와 관련되어 있는데 이를 확장 지점(extension point)
라고 합니다. 특정 라이프사이클 단계에 도달하게 되면 JUnit 엔진은 이미 등록된 extension들을 호출합니다.
즉, JUnit5에서 제공하는 라이프사이클을 확장할 수 있도록 도와주는 기능입니다. JUnit5에는 다음과 같은 확장 타입이 존재합니다.
이 중에서 이번에는 life-cycle callbacks
를 이용하려 합니다.
이 확장은 테스트의 라이프 사이클과 연관되어 있고 다음과 같은 인터페이스를 구현해서 정의할 수 있습니다.
현재 프로젝트에서는 테스트메서드를 실행한 후로 테이블을 비워주어야 하기 때문에 AfterEachCallBack
인터페이스를 구현하도록 하겠습니다.
public class DatabaseInitializerExtension implements AfterEachCallback {
@Override
public void afterEach(ExtensionContext context) {
DatabaseInitializer databaseInitializer = (DatabaseInitializer)SpringExtension
.getApplicationContext(context).getBean("databaseInitializer");
databaseInitializer.truncateTables();
}
}
테스트 환경의 context에서 databaseIntializer 빈을 가져와 truncateTables 메서드를 호출하도록 했습니다.
이제 이 Extension을 @ExtendWith
과 함께 사용해 간결해진 테스트코드를 확인할 수 있습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@ExtendWith(DatabaseInitializerExtension.class)
public @interface ApplicationTest {
}
이 방법말고도 간단하게 @DirtiesContext
를 사용하는 방법도 있습니다.
하지만 이 방법은 매번 컨텍스트를 다시 로드하기 때문에 시간이 많이 소요됩니다. 또한 @Nested
내부에 정의되어 있는 테스트에 대해 적용이 안되는 문제가 있습니다.
이를 해결하기 위해서는 아래와 같이 클래스 레벨에 애노테이션을 추가해주는 방법이 있습니다.
@NestedTestConfiguration(value = NestedTestConfiguration.EnclosingConfiguration.OVERRIDE)
JUnit5에서 제공해주는 Extension 덕분에 테스트코드가 간결해졌습니다. 다음 프로젝트에서도 적용해볼 방법이니 기억해둬야겠습니다!
https://giron.tistory.com/133
https://www.baeldung.com/junit-5-extensions