인수 테스트 성능 개선

홍성민·2023년 8월 6일
0

팀 201은 인수테스트의 가독성과 테스트 코드 재사용성을 높이기 위해 Cucumber 라이브러리를 사용했다.
이 라이브러리의 러닝 커브와 기존에 사용하던 RestAssured와는 다른 작동 방식 때문에 테스트 격리에 대한 퀄리티를 포기하고 @DirtiesContext를 사용하고 있었다.

기존의 ContextConfiguration 코드는 아래와 같다.

@CucumberContextConfiguration
@DirtiesContext
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = BackendApplication.class)
public class AcceptanceTest {

    @LocalServerPort
    int port;

    @Before
    public void before() {
        RestAssured.port = port;
    }
}

@DirtiesContext는 각 인수 테스트마다 새로운 컨텍스트를 띄우기 때문에 테스트 격리에 대한 신뢰도는 높지만 테스트 속도에 대한 이슈가 있었다.
개발 환경에서의 테스트, CI, CD의 속도를 개선하기 위해서 테스트 성능 향상은 필연적이었다.

고려사항

테스트 격리를 위한 가장 익숙한 방법으로 @Sql을 통해 테이블을 truncate하는 방식을 고려했다.
하지만 이 방법엔 몇가지 문제가 있다.
가장 큰 문제로 @Sql의 작동 시기를 예측할 수 없다는 점이 있다.
테스트 메서드 재사용성이 높은 Cucumber를 사용하면서 어느 메서드의 어느 시점에서 sql이 실행되는지 알 수 없기 때문에 정상적인 격리를 기대할 수 없었다.
두번째 문제는 테이블 변화에 따라 sql 파일 또한 수정해야하는 관리 지점이 생긴다는 점이다.

실제 적용

기존 프로젝트에서 jpa를 사용하고 있었기 때문에 EntityManager를 사용하는 방법을 채택했다.

변경된 ContextConfiguration은 아래와 같다.

@CucumberContextConfiguration
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = BackendApplication.class)
public class AcceptanceTest {

    @LocalServerPort
    int port;

    @Autowired
    private DatabaseCleaner databaseCleaner;

    @Before
    public void before() {
        RestAssured.port = port;
        databaseCleaner.clean();
    }
}

테스트 격리 책임을 가진 DatabaseCleaner를 테스트 환경에서 빈으로 등록하고 테스트가 이루어지기 전에 테이블을 비우는 방법이다.

DatabaseCleaner

@Profile("test")
@Component
public class DatabaseCleaner {

    @PersistenceContext
    private EntityManager entityManager;

    private List<String> tables;

    @PostConstruct
    public void init() {
        this.tables = entityManager.getMetamodel().getEntities().stream()
                                   .map(EntityType::getName)
                                   .map(this::toSnake)
                                   .toList();
    }

    private String toSnake(String camel) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < camel.length(); i++) {
            char c = camel.charAt(i);
            if (c >= 'A' && c <= 'Z' && i != 0) {
                sb.append('_');
            }
            sb.append(c);
        }
        return sb.toString().toLowerCase();
    }

    @Transactional
    public void clean() {
        entityManager.createNativeQuery("set foreign_key_checks = 0").executeUpdate();

        for (String table : tables) {
            entityManager.createNativeQuery("truncate table " + table).executeUpdate();
        }

        entityManager.createNativeQuery("set foreign_key_checks = 1").executeUpdate();
    }
}

EntityManager를 빈으로 주입받아 빈 초기화 콜백 메서드에서 Entity들을 찾고 snake case로 바꿔주는 과정을 거친 후 tables 필드로 저장한다.
@Sql을 사용했을 때 단점인 Entity와 테이블 변화에 영향을 받지 않는다.
clean 메서드에서 외래키 제약 조건을 해제하고 각 테이블을 truncate한 후에 외래키 제약 조건을 다시 걸어준다.

성능 변화

각각 entrypoint를 intellij에서 돌렸을 때, ./gradlew clean test를 통해 전체 테스트를 돌렸을 때이다.

기존 테스트 성능


변경 후 테스트 성능


profile
안녕하세요

1개의 댓글

comment-user-thumbnail
2023년 8월 6일

성능 개선의 과정이 잘 나타나있는 것 같아요. 잘 읽었습니다👍

답글 달기