@MybatisTest 클래스 레벨의 @Sql 이 수행되지 않는 경우

 

@MybatisTest 클래스 레벨의 @Sql 이 수행되지 않는 경우

문제 상황

  • MybatisTest 에서 메서드 레벨에서 @Sql  을 수행한 경우 클래스 레벨의  @Sql 이 동작하지 않는 상황 발견
  •  테스트 코드의 신뢰성에 문제가 생기는 경우가 발생하였다.

공식문서

문서상 @Sql  에 대해

  • 해당 애너테이션은 클래스 레벨과 메서드 레벨에서 동시에 선언하는 경우 기본적으로 메서드 레벨의 @Sql  선언이 클래스 레벨의 선언을 오버라이딩한다.
  • @SqlMergeMode  어노테이션을 통해 
    • 해당 동작을 변경할 수 있음.

이전 상황 

@Sql(statements = {"SELECT * FROM ORDERS",
    "UPDATE ORDERS SET DELETED_DATE = NOW() WHERE ID = 1"})
@DisplayName("주문 다건 조회(selectOrdersWithPaginationAndNotDeleted) -> 삭제되지 않은 1건 조회 정상 조회")
void When_SelectOrdersWithPaginationAndNotDeleted_Expect_ReturnOrdersWithPaginationAndReturnTwoOrders() {
    // given as @Sql
    int pageNum = 0;
    int pageSize = 10;
    
    OrderPageRequestDTO requestDTO = mock(OrderPageRequestDTO.class);
    given(requestDTO.getPageNum()).willReturn(pageNum);
    given(requestDTO.getPageSize()).willReturn(pageSize);
    
    // when
    List<Order> orders = orderMapper.selectOrdersWithPaginationAndNotDeleted(requestDTO);
    
    // then
    assertThat(orders).isNotNull();
    assertThat(orders.size()).isEqualTo(1);
}
  • @Sql  어노테이션이 오버라이딩을 해버리기에 -> @SqlMergeMode로⁠ 변경을 해보았음.

변경 후

@Test
@SqlMergeMode(SqlMergeMode.MergeMode.MERGE)
@Sql(statements = "UPDATE ORDERS SET DELETED_DATE = NOW() WHERE ID = 1")
@DisplayName("주문 다건 조회(selectOrdersWithPaginationAndNotDeleted) -> 삭제되지 않은 1건 조회 정상 조회")
void When_SelectOrdersWithPaginationAndNotDeleted_Expect_ReturnOrdersWithPaginationAndReturnTwoOrders() {
    // given as @Sql
    int pageNum = 0;
    int pageSize = 10;
    
    OrderPageRequestDTO requestDTO = mock(OrderPageRequestDTO.class);
    given(requestDTO.getPageNum()).willReturn(pageNum);
    given(requestDTO.getPageSize()).willReturn(pageSize);
    
    // when
    List<Order> orders = orderMapper.selectOrdersWithPaginationAndNotDeleted(requestDTO);
    
    // then
    assertThat(orders).isNotNull();
    assertThat(orders.size()).isEqualTo(1);
}
  • 테스트 정상 통과!
    • 테스트 마다 별도의 조건을 Sql 로 직접 주고 싶은 경우 해당 어노테이션으로 줄 수 있게 되었다.
 
 

여기서 잠깐..

  • @SqlMergeMode 어노테이션의 원리는 어떤식일까?
    • @Target({ElementType.TYPE, ElementType.METHOD})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Inherited
      public @interface SqlMergeMode {
      
          /**
           * Indicates whether method-level {@code @Sql} annotations should be merged
           * with class-level {@code @Sql} annotations or override them.
           */
          MergeMode value();
      
      
          /**
           * Enumeration of <em>modes</em> that dictate whether method-level {@code @Sql}
           * declarations are merged with class-level {@code @Sql} declarations.
           */
          enum MergeMode {
      
             /**
              * Indicates that method-level {@code @Sql} declarations should be merged
              * with class-level {@code @Sql} declarations, with class-level SQL
              * scripts and statements executed before method-level scripts and
              * statements.
              */
             MERGE,
      
             /**
              * Indicates that method-level {@code @Sql} declarations should override
              * class-level {@code @Sql} declarations.
              */
             OVERRIDE
      
          }
      
      }
      • 일단 내부적으로 MergeMode 라는 ENUM 타입의 내부 클래스를 가지고 있다.
      • 일단 @Sql  의 경우 SqlScriptsTestExecutionListener⁠에 의하여 테스트 실행 중에 SQL 스크립트를 연결할 수 있으며,
        • 해당 리스너는 해당 애너테이션을 처리해 지정된 시점에 SQL 스크립트를 실행할 수 있도록 도와준다.

        • public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListener {
          
              private static final Log logger = LogFactory.getLog(SqlScriptsTestExecutionListener.class);
      • 머지 코드의 감지
        • /**
           * Determine if method-level {@code @Sql} annotations should be merged with
           * class-level {@code @Sql} annotations.
           */
          private boolean mergeSqlAnnotations(TestContext testContext) {
              SqlMergeMode sqlMergeMode = getSqlMergeModeFor(testContext.getTestMethod());
              if (sqlMergeMode == null) {
                 sqlMergeMode = getSqlMergeModeFor(testContext.getTestClass());
              }
              return (sqlMergeMode != null && sqlMergeMode.value() == MergeMode.MERGE);
          }
          • 테스트 메서드에서 sqlMergeMode 어노테이션이 있는지 검증하고
          • MergeModeMERGE  라면 true  를 반환한다.
      • 스크립트 실행 부
        • /**
           * Execute SQL scripts configured via {@link Sql @Sql} for the supplied
           * {@link TestContext} and {@link ExecutionPhase}.
           */
          private void executeSqlScripts(TestContext testContext, ExecutionPhase executionPhase) {
              Method testMethod = testContext.getTestMethod();
              Class<?> testClass = testContext.getTestClass();
          
              if (mergeSqlAnnotations(testContext)) {
                 executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
                 executeSqlScripts(getSqlAnnotationsFor(testMethod), testContext, executionPhase, false);
              }
              else {
                 Set<Sql> methodLevelSqlAnnotations = getSqlAnnotationsFor(testMethod);
                 if (!methodLevelSqlAnnotations.isEmpty()) {
                    executeSqlScripts(methodLevelSqlAnnotations, testContext, executionPhase, false);
                 }
                 else {
                    executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
                 }
              }
          }
          • merge 모드라면
            •        executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
                     executeSqlScripts(getSqlAnnotationsFor(testMethod), testContext, executionPhase, false);
              • 클래스 레벨에서 한번
              • 메서드 레벨에서 한번 스크립트가 수행된다.
              • 머지라곤 하지만, 그냥 순차적으로 스크립트 수행이라 
                • 머지  의 의미가 맞나? 싶다.
                • 사실상 추가  의 의미가 더 강한 것 같다.