ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [테스트 격리] 일관된 테스트 격리를 적용하는 트러블 슈팅
    백엔드/Spring 2023. 8. 6. 01:56
    반응형

     

    제 팀은 각 테스트 종류마다 테스트 격리를 아래와 같이 진행했습니다.

     

    1. 인수 테스트 : BeforeEachCallback을 사용한 DatabaseCleaner

    2. 서비스 테스트 : @Transactional

    3. 리포지토리 테스트 : DatabaseCleaner

    4. 도메인 테스트 : 없음

     

    여기까지만 보면 바로 문제를 파악하신 분들도 있겠네요 😅

     


     

     

    1. 테스트 코드가 실패하다


    아무런 문제 없이 프로젝트를 진행하고 있던 어느 날, 느슨해진 테스트 코드 작성에 긴장감을 불어넣는 상황이 발생했습니다.

    로그인 기능을 구현하는 브랜치에서 테스트를 돌리는데, 서비스 테스트 1개가 실패하는 것...!!

     

    로그인아 무슨 일이니.. 나한테 왜 그러니..

    처음에는 서비스 메서드 자체에 문제가 있는 줄 알았습니다. 하지만 생각해보니 만약 메서드 로직이 잘못 됐으면 테스트가 딱 1개가 실패하는 것이 아니라 해당 메서드가 사용된 모든 테스트가 실패했어야 합니다.

    게다가 서비스 메서드 코드가 굉장히 간단했던 상황이라 메서드에 있는 코드에서 문제가 발생할리가 없었습니다.

     

    그러면 제가 모르는 다른 문제로 인해 테스트가 실패한다는 것이었습니다.

    테스트 로그를 확인해보니 저는 서비스 메서드 실행에서 문제가 발생할 줄 알았는데, 다른 곳에서 문제가 발생하고 있었습니다.

     

    Hibernate: 
        select
            member0_.id as id1_1_,
            member0_.nickname as nickname2_1_,
            member0_.platform_id as platform3_1_,
            member0_.profile_image as profile_4_1_ 
        from
            member member0_
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_1_] : [BIGINT]) - [1]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([nickname2_1_] : [VARCHAR]) - [test1]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([platform3_1_] : [VARCHAR]) - [null]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([profile_4_1_] : [VARCHAR]) - [test1.png]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_1_] : [BIGINT]) - [2]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([nickname2_1_] : [VARCHAR]) - [test2]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([platform3_1_] : [VARCHAR]) - [null]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([profile_4_1_] : [VARCHAR]) - [test2.png]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_1_] : [BIGINT]) - [3]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([nickname2_1_] : [VARCHAR]) - [test3]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([platform3_1_] : [VARCHAR]) - [null]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([profile_4_1_] : [VARCHAR]) - [test3.png]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_1_] : [BIGINT]) - [4]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([nickname2_1_] : [VARCHAR]) - [test4]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([platform3_1_] : [VARCHAR]) - [null]
    2023-07-27 14:07:53.726 TRACE 23622 --- [    Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([profile_4_1_] : [VARCHAR]) - [test4.png]

     

    테스트가 시작할 때, 위 4개의 이상한 데이터들이 자동으로 추가된다는 것이었습니다.

     

    서비스 테스트 코드를 전체적으로 살펴보았는데, 신기하게도 test3 유저까지 만드는 테스트 코드는 존재했지만, test4 유저를 만드는 코드는 서비스 테스트 코드 어디에서도 존재하지 않았습니다.

     

    이 말은 어디선가 데이터가 격리되지 않았다는 말이었습니다.

    하지만 분명 코드상으로는 테스트 격리를 처리했는데..??

     

     

    다행히도 test4 유저를 만드는 테스트 코드는 전부 인수 테스트에 있어서 문제 원인을 파악하기 수월했습니다.


     

     

     

    2. DatabaseCleaner vs @Transactional 테스트 격리 방식


    펀잇 팀에서는 테스트 종류마다 테스트 격리를 다르게 진행해왔습니다.

     

    1. 인수 테스트 : BeforeEachCallback을 사용한 DatabaseCleaner

    2. 서비스 테스트 : @Transactional

    3. 리포지토리 테스트 : DatabaseCleaner

    4. 도메인 테스트 : 없음

     

    DatabaseCleaner는 JUnit의 BeforeEachCallback Interface를 사용하는 구현체입니다. 

    이걸 만든 이유는 @Sql로 테스트 격리를 하게 된다면 데이터를 초기화하는 SQL문을 직접 작성해야 한다는 불편 사항이 있습니다. 하지만 해당 코드는 최대한 자바 코드를 사용하여 SQL문을 최소한으로 줄이고, 자바 코드를 통해 데이터를 초기화 할 수 있는 객체입니다.

     


     

    [BeforeEachCallback 테스트 격리 방식]

    https://junit.org/junit5/docs/5.7.1/api/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeEachCallback.html

     

    BeforeEachCallback (JUnit 5.7.1 API)

    BeforeEachCallback defines the API for Extensions that wish to provide additional behavior to tests before an individual test and any user-defined setup methods (e.g., @BeforeEach methods) for that test have been executed. Concrete implementations often im

    junit.org

     

    BeforeEachCallback에 대해 짧은 요약을 하자면 @BeforeEach가 실행되기 전에 실행될 수 있는 인터페이스입니다.

    관련 인터페이스는 총 6가지가 있습니다.

    - BeforeAllCallback : @BeforeAll 실행 전에 수행

    - AfterAllCallback : @AfterAll 실행 후에 수행

    - BeforeEachCallback : @BeforeEach 실행 전에 수행

    - AfterEachCallback : @AfterEach 실행 후에 수행

    - BeforeTestExecutionCallback : @BeforeEach 실행 후에 수행

    - AfterTestExecutionCallback : @AfterEach 실행 전에 수행

     

    위 6개 모두 Extension 인터페이스를 가지고 있기 때문에 찾기 수월합니다.

     

    어쨌든 DatabaseCleaner를 사용하게 된다면 BeforeEachCallback을 구현했으니 테스트 메서드가 실행되기 전에 모든 데이터를 초기화하게 됩니다.

     


    [@Transactional 테스트 격리 방식과의 차이점]

    트랜잭션은 다들 알다시피 트랜잭션 내부에 모든 작업이 끝나서야 Commit을 하여 작업사항을 저장하거나, 트랜잭션 내부 작업 도중에 Rollback을 하여 트랜잭션 내부에서 진행되었던 모든 작업을 취소할 수 있습니다.

     

    이중에서 테스트에서 @Tranasactional은 Rollback을 수행하여 테스트 격리가 수행됩니다.

     

    정리하면 DatabaseCleaner테스트 메서드 수행 전에 데이터 초기화를 진행하고, @Transactional테스트 메서드 수행 후에 데이터 초기화를 진행합니다.

     

    바로 여기서 문제가 발생하게 됩니다.

     

    마지막 인수 테스트가 끝나면 데이터가 남아있는 상태에서 첫번째 서비스 테스트가 진행되기 때문에 문제가 발생했던 것 입니다.

    이거는 정말 운이 좋게도 인수 테스트에 저장된 데이터가 서비스 테스트에서도 해당 데이터를 통해 테스트하는 상황이라 발견했던 것이지, 만약 다른 서비스 테스트가 먼저 수행되었으면 에러가 발생이 안됐을 수도 있었습니다.

    나중에 많은 테스트 코드가 만들어지고 나서 발견이 되었으면 디버깅하는데 오랜 시간이 걸렸을 것 같네요. 


     

     

     

     

    3. 일관된 테스트 격리 수행하기


    인수 테스트 → 서비스 테스트로 넘어가는 과정에서 격리가 진행되지 않은 문제를 발견했으니 이제 해결할 일만 남았습니다.

     

    제 팀은 일관된 테스트 격리를 방식을 수행하기 위해 서비스 테스트에서도 DatabaseCleaner를 적용하도록 하였습니다.

    이렇게 되면 도메인 단위 테스트를 제외한 모든 테스트에 테스트 격리가 수행되는 것이죠

     

    이러면 일관된 방식의 테스트 격리가 적용되기 때문에 위에 있던 문제의 그림이 아래처럼 바뀌게 됩니다.

     

    이렇게 하여 일관된 테스트 격리를 진행하게 되었습니다.

     


     

     

    글을 쓰다가 알게 됐는데, DatabaseCleaner를 AfterEachCallback이나 AfterTestExecutionCallback으로 바꾸면 서비스 테스트에서는 DatabaseCleaner를 추가하지 않아도 되겠네요.

     

    테스트 격리 방식은 @Transactional, @Sql, JUnit Extension 등 다양한 격리 방법이 존재하니 자신의 팀에 테스트 격리가 제대로 되어있는지 파악을 해보면 좋을 것 같습니다.

    반응형

    댓글

Designed by Tistory.