들어가며
Repository 테스트를 하던 중, 개별 테스트는 성공하는데 전체 테스트가 실패하는 현상이 발생해, 이를 해결하고 정리한 글이다.
Repository 단위 테스트 구성
Repository 테스트는 아래처럼 RepositoryTest 클래스를 만들어서 상속하도록 구성했다.
이렇게 구성한 이유는 아래와 같다.
- 모든 테스트 클래스마다 애노테이션을 매번 붙이기 번거롭다.
- 공통으로 의존성을 주입해야 하는 것들을 한 번만 작성하면 된다. (ex TestEntityManager 등)
- @BeforeEach 등을 상위 클래스에 정의해주면 이를 상속하는 모든 클래스에 적용된다.
- static 으로 공통 설정을 관리할 수 있다. (TestContainers 등)
@DataJpaTest
@ExtendWith(SpringExtension.class)
@AutoConfigureTestDatabase(replace = NONE)
@Import(RepositoryTestConfig.class)
@ActiveProfiles("test")
@Getter
@Sql(scripts = "classpath:sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public abstract class RepositoryTest {
...
}
문제 발생 상황
여기서 문제가 발생한 부분이 @Sql 이다. @Sql 에는 Inherited 가 있기 때문에 상위 클래스에 선언된 경우 하위 클래스에도 적용된다. 따라서 RepositoryTest 를 상속받은 테스트 클래스에서도 @Sql 이 적용될 것이라 생각했다.
기대했던 동작은 아래와 같다. 테스트 클래스마다 초기화 스크립트가 필요하다면 클래스 최상단에 @Sql 을 붙여주고, 공통적인 데이터 정리는 상위 클래스의 @Sql 에서 한다.
// OrderRepositoryTest의 @Sql에 의해 매 테스트 케이스 실행 전 초기 데이터 삽입
@Sql(scripts = "classpath:sql/setup_order.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
// RepositoryTest 클래스에서 상속받은 @Sql에 의해 매 테스트 케이스 실행 후 데이터 정리
@Sql(scripts = "classpath:sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
그런데, 전체 테스트를 돌려보니 테스트가 깨지는 문제가 발생했다.
ScheduleRepository 에서 테스트가 실패한 것을 볼 수 있다.
테스트에 문제가 있는건지 확인하기 위해 ScheduleRepository 개별로 돌려보니 모든 테스트가 통과한다.
개별 테스트는 정상적으로 실행되는데, 전체 테스트가 깨진다는 것은 테스트 클래스 간 데이터 격리가 제대로 이루어지지 않았다는 것을 의미한다. 그런데, 위에서처럼 @Sql 애노테이션을 이용해 cleanup.sql 을 호출하는데, 왜 데이터가 충돌할까?
개별 테스트가 성공했기 때문에 cleanup.sql 에서 데이터 초기화를 해주고 있다는 의미다. 만약 cleanup.sql 이 제대로 동작하지 않았다면, 테스트클래스 내부의 테스트들이 데이터 충돌로 인해 제대로 실행되지 않았을 것이다.
검증해보기
먼저, 전체 테스트를 실행시켰을때 실패한 테스트메서드를 확인해 보았다.
@Nested
@DisplayName("findDailyOrders 메서드는")
class Describe_findDailyOrders {
@Nested
@DisplayName("특정 날짜가 주어진 경우")
class Context_with_specific_date {
final DailyCond cond = DailyCond.builder()
.year(today.getYear())
.month(today.getMonthValue())
.day(today.getDayOfMonth())
.teamMember(ordererVo)
.build();
private List<QuerySchedule> schedules;
@BeforeEach
void setUpContext() {
schedules = scheduleRepository.findDailyOrders(cond);
}
@Test
@DisplayName("주문목록을 반환해야 한다.")
void it_returns_daily_orders() {
assertThat(schedules.size()).isEqualTo(1);
}
}
}
특정 날짜에 해당하는 주문목록을 반환하는 테스트다.
왜 Failed 되었는지 보기 위해 JUnit 리포트를 확인해보았다. build/reports/tests/test/index.html 경로에서 리포트를 확인할 수 있다.
주문목록이 1개여야 하는데 0개로 나온다.
Junit 리포트에서는 SQL 로그를 볼 수 없기 때문에 왜 0 개가 나오는지 확인해보기 위해서는 SQL 로그를 확인해야 한다.
application-test.yml 에서 hibernate 로그옵션을 아래처럼 설정했다.
logging:
level:
hibernate:
SQL: DEBUG
orm.jdbc.bind: TRACE
type.descriptor.sql: TRACE
- hibernate.SQL: DEBUG : SQL 쿼리를 출력
- orm.jdbc.bind: TRACE : 쿼리에 바인딩된 파라미터 값 출력
- type.descriptor.sql: TRACE : SQL 타입 변환 및 매핑정보 출력 (예를 들어 String 타입이라면 VARCHAR 로 변환되는 타입 정보 확인 용도)
출력 로그가 많기 때문에 테스트를 실행할 때 나오는 로그를 파일로 저장해두고, 해당 메서드부분을 로그파일에서 검색해서 찾아보았다.
./gradlew :yellobook-domain:test --info > test-log.txt
문제가 되는 select 문이다.
해당하는 데이터의 insert 문이다.
insert 문에서는 member_id 가 5 인데, select 문에서는 member_id = 2 로 select 하고 있기 때문에 0개가 나온 것이다.
다음으로 테스트 실행순서를 확인해보았다. OrderRepository 테스트가 실행되고, 이후에 ScheduleRepository 테스트가 실행된다.
원인이 뭘까?
여기까지 확인하고 종합해봤을 때 아래처럼 문제가 발생한 것임을 짐작할 수 있었다.
- OrderRepository 에서 cleanup.sql 이 실행되지 않았기 때문에 데이터가 지워지지 않고 남아 있다.
- 이후에 수행되는 ScheduleRepository 테스트에서 첫 번째 테스트가 findDailyOrders 메서드 테스트다.
- 데이터를 삽입할 때 TestEntityManager 로 삽입하므로 id 값을 비워놓고 persist 하는데, OrderRepository 를 테스트할 때 만들어둔 사용자가 3명이라 member_id 값이 2가 아닌 5로 조회한다.
- findDailyOrders 테스트가 끝나면 cleanup.sql 이 실행되어 이후 테스트들은 모두 성공한다.
위 짐작이 맞다면 OrderRepository 테스트에서는 cleanup.sql 이 동작하지 않고, ScheduleRepository 테스트에서는 동작한다는 의미인데, 정말일까?
cleanup.sql 이 제대로 실행되는지 확인해 볼 필요가 있다. @Sql 실행을 확인하는 방법은 공식문서 Logging SQL Scripts and Statements 에서 찾을 수 있었다.
application-test.yml 에 아래처럼 추가해준다. 이렇게 추가해놓으면 sql 파일 실행여부와 어떤 sql 이 발생했는지 확인할 수 있다.
logging:
level:
org:
springframework:
test:
context:
jdbc: DEBUG
jdbc:
datasource:
init: DEBUG
ScheduleRepository 테스트를 실행시켜보면 아래처럼 cleanup.sql 이 실행되는 것을 확인할 수 있다.
그런데, OrderRepository 테스트를 실행하면 setup_order.sql 은 실행되지만, cleanup.sql 이 실행되지 않았다.
문제 해결
스택오버플로우에서 관련된 답변을 찾을 수 있었다. Merging @Sql from superclass with @Sql in subclass
상위 클래스에 정의된 @Sql 설정은 하위클래스의 @Sql 과 병합되지 않는다. 즉, 상위클래스에 @Sql 이 정의되어 있더라도, 하위클래스에서 @Sql 을 다시 정의하면 상위클래스 설정을 덮어쓴다.
아래처럼 cleanup.sql 이 부모클래스에 존재하는 상태에서 자식클래스에 @Sql 을 사용하면 RepositoryTest 클래스의 @Sql 을 덮어쓴다.
@Sql(scripts = "classpath:sql/setup_order.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@DisplayName("OrderRepository Unit Test")
public class OrderRepositoryTest extends RepositoryTest {
...
}
OrderRepositoryTest 를 개별로 실행했을때 성공했던 이유는 @DataJpaTest 에서 @Transactional 이 있기 때문에 데이터가 롤백되었고, cleanup.sql 에서 초기화해주는 AUTO_INCREMENT (id 값) 를 해당 테스트클래스에서 이용하고 있지 않았기 때문이다.
다시말해 운좋게 테스트가 통과한 경우다.
cleanup.sql 을 이용하기 위해서는 아래처럼 ScriptUtils 를 사용해서 해결할 수 있다. 이 경우 @Sql 이 아니라, @AfterEach 를 이용한다.
@AfterEach
void tearDown() {
try {
ScriptUtils.executeSqlScript( Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection(), new ClassPathResource("/sql/clean.sql"));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
'Spring' 카테고리의 다른 글
[Spring] TestContainers, @TestConfiguration 사용시 @DynamicPropertySource 가 적용되지 않는 문제와 해결방안 (0) | 2025.04.10 |
---|---|
[Spring] API 응답에서 직접 정의한 Error code 는 왜 사용할까? (0) | 2025.03.16 |
[Spring] OpenApI 3.0 Swagger 문서 작성 및 설정 방법 (0) | 2025.02.11 |
[SpringBoot] MapStruct 제대로 활용하기 (0) | 2024.11.03 |
[SpringBoot] 데이터베이스 마이그레이션 툴 Flyway 도입기 (0) | 2024.09.28 |