[Test] 테스트별 DB환경 초기화

JeongYong Park·2023년 7월 31일
1

이번 issue-tracker 프로젝트를 진행하면서 테스트별로 독립된 환경을 제공할 필요가 있었습니다. 이를 위해 테스트별로 DB환경을 초기화해주면 좋겠다 싶어서 자동으로 DB환경을 초기화해주는 코드를 작성하게 되었습니다.

글을 작성하기에 앞서 Java11, Spring Boot 2.7.14, spring-boot-starter-jdbc를 사용하였음을 알립니다.

@AfterEach

우선은 @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

JUnit5 Extension은 테스트 실행 중 특정 이벤트와 관련되어 있는데 이를 확장 지점(extension point)라고 합니다. 특정 라이프사이클 단계에 도달하게 되면 JUnit 엔진은 이미 등록된 extension들을 호출합니다.
즉, JUnit5에서 제공하는 라이프사이클을 확장할 수 있도록 도와주는 기능입니다. JUnit5에는 다음과 같은 확장 타입이 존재합니다.

  • test instance post-processing
  • conditional test execution
  • life-cycle callbacks
  • parameter resolution
  • exception handling

이 중에서 이번에는 life-cycle callbacks를 이용하려 합니다.

Lifecycle Callbacks

이 확장은 테스트의 라이프 사이클과 연관되어 있고 다음과 같은 인터페이스를 구현해서 정의할 수 있습니다.

  • BeforeAllCallBack & AfterAllCallBack
    • 모든 테스트 메서드들이 실행되기 전에 실행됩니다. (@BeforeAll 전, @AfterAll 후)
  • BeforeEachCallBack & AfterEachCallback
    • 각 테스트 메서드들이 실행되기 전, 후로 실행됩니다. (@BeforeEach 전, @AfterEach 후)
  • BeforeTestExecutionCallback and AfterTestExecutionCallback
    • 테스트 메서드를 실행하기 전, 후로 즉시 실행됩니다. (테스트 메서드 실행 전, 후)

AfterEachCallBack 적용

현재 프로젝트에서는 테스트메서드를 실행한 후로 테이블을 비워주어야 하기 때문에 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에서는 테스트 라이프사이클의 확장을 위한 기능을 제공해줍니다.
  • 제공하는 인터페이스를 구현하는 방법으로 적용해볼 수 있습니다.

JUnit5에서 제공해주는 Extension 덕분에 테스트코드가 간결해졌습니다. 다음 프로젝트에서도 적용해볼 방법이니 기억해둬야겠습니다!

참고 자료

https://giron.tistory.com/133
https://www.baeldung.com/junit-5-extensions

profile
다음 단계를 고민하려고 노력하는 사람입니다

0개의 댓글