- 프로젝트 수행중
- 통합 테스트와 단위 테스트 간의 데이터베이스의 충돌이 발생하는 일이 생겼다.
@Sql
을 이용하여- 통합테스트에서 별도 테스트 데이터를 생성한 이후에
- @Rollback 을 시도하였지만
- 별도로 초기화가 수행되지 않는 것 같다..
- 이유는 솔직히 잘 모르겠다… 솔직히 파고들면 이건 끝도 없을거같아 일단 패스함..
- 일단 테스트 후에 데이터베이스를 비우는 행위 자체는 일단 가능은 하다.
- Spring Boot 에서는
@DirtiesContext
어노테이션을 사용해 테스트 후 Spring Application Context 를 재 로드 할 수 있다.
- 테스트후에 사용된 데이터는 초기 상태로 돌아가게 되는 것이다.
@SpringBootTest
의 경우 컨텍스트를 로드하게 이전에는 발생하지 않던 문제가 발생하는 것이 아닐까 생각된다.
- 일단은..
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
테스트 코드에 성능에 문제가 있지 않을까라는 생각은 든다.
하지만 일단 당장 통합테스트의 구현을 위해 추가해본다.
하지만.. 이후에도 실패했다.
- 컨텍스트로드는 컨텍스트 로드이지 DB 초기화의 역할은 수행하지 않는다..
- 혹시나 싶어서 기대했건만..
- 진짜 AfterEach 로 일일이 제어해야하는건가?..
- 탈모올것같다.
일단, @Rollback의 경우 트랜잭션의 영향을 받는다.
- 하지만 비동기적인 테스트 수행의 경우 각각 독립적인 트랜잭션을 수행중이기에, 해당
@Rollback
어노테이션은 정상적으로 동작하지 않을 수 있다고한다.
- 사용하는 것이 의미가 없는 것이다.
@SpringBootTest에서 롤백이 되지 않는 이유
- 테스트 수행후에 테이블을 아예 truncate 해버리면 어떨까 라는 생각이 들었다.
- 그래서
#custom table schema
customed.table.schema = item-browser
@Value("${customed.table.schema}")
private String tableSchema;
@AfterEach
void cleanUpAfterEachTest() {
String selectAllTableName = MessageFormat.format("SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ';') " +
"FROM INFORMATION_SCHEMA.TABLES " +
"WHERE table_schema = '{0}' " +
"AND table_type = 'BASE TABLE' ", tableSchema);
List<String> truncateTableSqls = jdbcTemplate.queryForList(selectAllTableName, String.class);
truncateTableSqls.forEach(jdbcTemplate::execute);
}
- 테이블 메타데이터에서 모든 테이블을 검색한 뒤
- 모든 테이블에 대한 truncate 를 수행하는 쿼리를 조합한다.
- 그 후에 모든 쿼리를 수행시켜버리는것이다
- 물론 이 방법도 만약 spring.profile 이 변경되는 경우
진짜 매우 심각한
- 위험이 발생한다
- 그렇기에 이러한 방어 어노테이션을 삽입했다
@SpringBootTest
@IfProfileValue(name = "spring.profiles.active", value = "local")
public class OrderDeleteIntegrationTest {
- 프로파일이 local 로 활성화된 경우에만 해당 통합테스트가 돌도록 수행했다.
Message.format 으로 SQL 을 만드는 경우
- 쿼리의 작은 따옴표와 중괄호가 충돌할 수 있다고 한다.
- 그래서 작은 따옴표를 일단 이스케이핑 처리를 해야한다고한다.
- 이스케이핑 처리를 하면 일단 쿼리가 너무 더러워 질거같아
String.format 으로 변경했다.
String selectAllTableName = String.format("SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ';') " +
"FROM INFORMATION_SCHEMA.TABLES " +
"WHERE table_schema = '%s' " +
"AND table_type = 'BASE TABLE' ", tableSchema);
그리고 truncate 를 수행하는 경우 제약조건에 영향을 받을 수 있기에
@AfterEach
void cleanUpAfterEachTest() {
String selectAllTableName = String.format("SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ';') " +
"FROM INFORMATION_SCHEMA.TABLES " +
"WHERE table_schema = '%s' " +
"AND table_type = 'BASE TABLE' ", tableSchema);
jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0;");
List<String> truncateTableSqls = jdbcTemplate.queryForList(selectAllTableName, String.class);
truncateTableSqls.forEach(jdbcTemplate::execute);
jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1;");
}
- 최종적으로 이러한 코드가 되었다.
일단
- 통합테스트 단건 자체는 통과했다
- 이후에 단위테스트와 동시에 진행해보자
햐
- 모든 테스트를 통과했다.
하지만 모든 테스트에 이러한 코드를 추가하는 바보같은 행동을 지속적으로 할 순없다.
이후 해당 메서드는 별도 클래스로 분리후에 별도 어노테이션으로 주입될 수 있도록 되어야 할 것 같다.
11.26 추가
- 스레드의 성공 외의 실패의 케이스도 수정을 했다.
@Test
@Sql(scripts = {"classpath:/sql/member/insert_member.sql", "classpath:/sql/shippinginfo/insert_shipping_info.sql", "classpath:sql/product" +
"/insert_product.sql", "classpath:sql/order/insert_order_product.sql",
"classpath:sql" + "/order/insert_order.sql"})
@DisplayName("5개의 스레드로 동시에 주문 취소를 수행하는 경우, 1개의 스레드는 성공하고 나머지는 실패해야 합니다.")
public void When_5ThreadsDeleteSameOrder_Expect_OnlyOneSuccessAnd() throws InterruptedException {
// given - @Sql
int threadCount = 5;
int expectedSuccessCount = 1;
int expectedFailCount = threadCount - expectedSuccessCount;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger();
List<Future<?>> futures = new ArrayList<>();
// when
for (int i = 0; i < threadCount; i++) {
futures.add(executorService.submit(() -> {
orderService.removeOrder(1L);
successCount.incrementAndGet();
}));
}
executorService.shutdown();
boolean didAllThreadsTerminate = executorService.awaitTermination(1, MINUTES);
// then
// 스레드가 모두 1분안에 수행된 경우에만 성공
assertThat(didAllThreadsTerminate).isTrue();
// 하나만 성공하는지 확인
assertThat(successCount.get()).isEqualTo(expectedSuccessCount);
// 스레드의 개수와 future 가 동일한지 확인
assertThat(futures.size()).isEqualTo(threadCount);
// 실패한 테스트의 경우 CustomIllegalStateException 이 발생하므로, 이를 통해 실패한 테스트를 확인
futures.stream()
.filter(Future::isDone)
.forEach(future -> {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
// CustomIllegalStateException 이 발생해야함 - 주문이 존재하지 않음
assertThatCode(() -> {
throw e.getCause();
}).isInstanceOf(CustomIllegalStateException.class)
.hasMessage(ErrorCode.ORDER_NOT_FOUND.getMessage());
// 실패 카운트 증대
failCount.getAndIncrement();
}
});
// 실패카운트가 4개인지 확인
assertThat(failCount.get()).isEqualTo(expectedFailCount);
}
- 찾아보니 future를 활용하는 방식이 잘못되었었다.
- Future::IsCancelled 는 해당 메서드의 실패에 대한 내용이 아니라
- 아예 Future 에 성공이나 예외를 담지 못한 케이스는 isCancelled() 가 true 를 반환하고 있던 것이다.
- IsDone 으로 일단 성공과 예외 케이스에 대해 필터링을 하고
- 해당 케이스에서 예외가 발생하면
- InterruptedException 혹은 ExecutionException 에 감싸서 예외가 던져진다고 한다.
- 일단 커스텀 예외는 e.getCause() 를 통하여 꺼내서 확인이 가능하기에,
- 해당 예외의 인스턴스와 메시지를 체크하는 방식으로 예외 체크를 진행했다.
- Future::IsCancelled 는 해당 메서드의 실패에 대한 내용이 아니라
- 이후에 실패 카운트의 증대 부분은
- 람다 안에서 별도로 카운트를 진행하기에
- 원자성을 지키기 위해
AtomicInteger
로 설정하였다..
- 원자성을 지키기 위해
- 람다 안에서 별도로 카운트를 진행하기에
Uploaded by N2T
'자바 > 리팩토링' 카테고리의 다른 글
[프로젝트] 스웨거 example 설정시 String.format 등의 동적 값 삽입이 불가능함 (0) | 2023.12.19 |
---|---|
[리팩토링] 스웨거 DTO 가 schema 에 보이지 않는 이유와 DTO 어노테이션 설정법 (0) | 2023.12.17 |
[리팩토링] ExecutorService 에서 execute 과 submit 은 각각 어떤 경우 사용해야될까? (0) | 2023.11.26 |
[리팩토링] ENUM 에 한글과 value 등등.. (0) | 2023.11.19 |
[리팩토링] 기획된 개발은 차라리 낫지.. (0) | 2023.11.17 |