박종훈 기술블로그

통합테스트를 진행하면서 부딪힌 이슈들 (일부 항목만 Mock으로 처리하기, BeforeAll Non-static 에서 사용하기, @SQLDelete 가 적용된 상태에서 테이블 초기화 하기)

오늘은 회사에서 서비스 클래스를 테스트 하고자 하였다. 그 과정에서 부딪힌 이슈들에 대해서 정리해본다.

1. 일부 항목만 Mock으로 처리하기

테스트 대상인 서비스 클래스에는 많은 컴포넌트들이 물려있었고, 그 중에는 이벤트 디스패쳐 (실행 결과를 이메일, 알림톡, SMS 등으로 발송 하는 컴포넌트) 도 포함되어있었다.

테스트 코드를 작성할 때 가능하면 목을 사용하지 않으려 한다. 하지만 비관리 의존성은 예외이다. 비관리 의존성을 간단하게 이야기하면 전체를 제어할 수 없는 프로세스 외부 의존성 을 의미한다.

비관리 의존성에 대해서는 아래 글에 정리되어 있다.

이벤트 디스패처는 비관리 의존성 이다. 따라서 mocking을 해도 적절한 경우이다.

서비스에는 @Autowired 로 의존성 주입을 시켜주고 이벤트 디스패처는 @MockBean 으로 목 객체로 만들어주었다.

2. BeforeAll Non-static 에서 사용하기

testcontainer를 직접 사용해보고 작성한 글에서 아래와 같은 내용을 언급하였다.

테스트 시작 시점에 데이터 정리하기: 이 방법이 가장 좋다. 빠르게 작동하고 일관성이 없는 동작을 일으키지 않으며, 정리 단계를 실수로 건너뛰지 않는다.

이 내용을 실제 구현으로 옮기기위해 BeforeAll을 사용하려고 하였는데 문제는 BeforeAll은 기본적으로 static만 허용된다. 그래서 아래 글을 참고해서 non-static한 BeforeAll을 구현하였다.

TestInstance 어노테이션을 이용하여 아래와 같이 코드를 구성할 수 있다.

@TestInstance(Lifecycle.PER_CLASS)
class TweetSerializerUnitTest {

    private String largeContent;

    @BeforeAll
    void setUpFixture() {
        // read the file
    }
}

3. @SQLDelete 가 적용된 상태에서 테이블 초기화 하기

SQLDelete이 적용되어 있으면 JPA를통해 Delete를 시도할 때 해당 어노테이션에 명시해둔 sql 문을 수행한다. 이를 통해 소프트 딜리트를 처리하였다.

하지만 나는 위에서 이야기 한 대로 테스트 시작 시점에 데이터 정리하기 를 하고 싶었기 때문에 어떻게든 데이터를 초기화 하고 싶었다.

그래서 발견한 것이 How to ignore a @SQLDelete annotation in certain cases 이 글에 달린 답변이였다.

JPQL을 통해서 수행하면 삭제가 가능하다는 것을 보여주는 답변이였는데 실제로 해보니 소개해준 createQuery 로는 해결되지는 않았다. 다만 em.createNativeQuery 으로는 되었다.

em.createNativeQuery("DELETE FROM user").executeUpdate();

위와같이 sql을 작성해서 수행하니 데이터가 잘 삭제 되었다. 하지만 이후 나온 문제가 auto increment 도 리셋이 되어야 완벽한 초기화가 된다는 것이였다.

em.createNativeQuery("TRUNCATE payment_method").executeUpdate();

그래서 Delete를 Truncate로 변경해서 수행하였다.

마지막으로 아래와 같이 유틸화까지 진행하였다.

public static void truncateTables(EntityManager em, String... tableNames) {
    if (tableNames.length == 0) {
        return;
    }

    em.getTransaction().begin();
    em.createNativeQuery("SET FOREIGN_KEY_CHECKS=0").executeUpdate();
    Arrays.stream(tableNames).forEach((tableName) -> em.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate());
    em.createNativeQuery("SET FOREIGN_KEY_CHECKS=1").executeUpdate();
    em.getTransaction().commit();
    em.close();
}

FOREIGN_KEY_CHECKS 를 넣어준 이유는 외래키로 인한 꼬임이 없도록 하기 위해서 추가하였고

이렇게 작성한 코드를 BeforeAll 에서 사용해주었다. 필요에 따라서 BeforeEach에서 사용해도 된다.

@BeforeAll
void setup() throws IOException {
    EntityManager em = emf.createEntityManager();
    DatabaseUtils.truncateTables(em, "table_a", "table_b", "table_c", ...);

    // ...
}

최대한 직접 손대는 것은 피하고 싶기 때문에 위와 같은 방식으로 직접 SQL을 작성하는 것은 데이터베이스 초기화에만 사용하였다. 나머지 부분들은 Repository를 이용해서 데이터를 구성하도록 처리하였다.