과정을 즐기자

테스트 코드 작성시 @Transactional 사용하지 않기 본문

테스트 코드

테스트 코드 작성시 @Transactional 사용하지 않기

320Hwany 2023. 3. 4. 15:22

테스트 코드에서 @Transactional은 데이터베이스도 롤백이 되기 때문에 @BeforeEach에서 일일이 

repository를 deleteAll 해주지 않고 외래키 문제로 고민할 필요도 없기 때문에 편리합니다.

그래서 자주 사용을 했었는데 한가지 문제점이 있습니다. 

바로 원래 코드에서 @Transactional이 없더라도 테스트 코드에서 @Transactional을 사용한다면 

하나의 트랜잭션으로 묶여 원래 코드와 다른 결과를 나타낼 수 있다는 점입니다.

 

이 문제점은 예전 글에서 작성한 적이 있습니다. 

 

 

JPA 변경감지 적용이 안될 때

JPA 변경 감지 적용이 안된다? 프로젝트에서 리뷰 수정 기능을 구현하고 있는데 JPA에서는 변경 감지(더티 체킹)를 작동한다는 사실을 알고 있었다. 영속성이 유지되는 엔티티라면 update 쿼리를 따

320hwany.tistory.com

'위 테스트가 실패를 했다. 이유는 테스트 코드에도 @Transactional을 적용해줘야 하기 때문이다.
@Transaction을 적용해주니 테스트가 통과하였다. 하지만 이때 Service의 @Transaction을 지워도
테스트에 @Transaction이 있어서 테스트가 통과하였다. 애플리케이션의 작동 여부를 체크하기 위한
테스트 코드인데 뭔가 찝찝한 느낌이다... 혹시 다른 방법이 있으려나... 찾으면 다시 글을 작성하기로..'

 

위 글에서 이러한 문장을 작성한 적이 있는데 더 좋은 방법을 찾아 이번 글에서 정리해보려고 합니다.

 

먼저 참고한 자료입니다 

 

[Spring] @SpringBootTest의 테스트 격리시키기(TestExecutionListener), @Transactional로 롤백되지 않는 이유

이번에 넥스트스텝 ATDD 강의를 듣게 되었습니다. 과제 중에 @SpringBootTest를 사용하는 테스트들을 격리시키는 부분이 있었는데, 제가 사용했던 방법을 공유하도록 하겠습니다. 1. SpringBootTest가 @Tran

mangkyu.tistory.com

 

위 글에서 말하는 것은 '모든 테스트를 서로 독립적으로 실행하고 격리시키기 위해 테이블 명세만 

남기고 모든 데이터를 제거하는 TRUNCATE 명령어를 모든 테이블에 수행하자' 라는 것 입니다.

위 글에서 정말 정말 자세히 설명이 되어있기 때문에 저는 간단히 사용법만 정리하겠습니다. 

 

AcceptanceTestExecutionListener 

public class AcceptanceTestExecutionListener extends AbstractTestExecutionListener {

    @Override
    public void afterTestMethod(final TestContext testContext) {
        final JdbcTemplate jdbcTemplate = getJdbcTemplate(testContext);
        final List<String> truncateQueries = getTruncateQueries(jdbcTemplate);
        truncateTables(jdbcTemplate, truncateQueries);
    }

    private JdbcTemplate getJdbcTemplate(final TestContext testContext) {
        return testContext.getApplicationContext().getBean(JdbcTemplate.class);
    }

    private List<String> getTruncateQueries(final JdbcTemplate jdbcTemplate) {
        return jdbcTemplate.queryForList("SELECT Concat('TRUNCATE TABLE ', TABLE_NAME,
        ';') " + "AS q FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA
        			= 'PUBLIC'", String.class);
    }

    private void truncateTables(final JdbcTemplate jdbcTemplate,
    							final List<String> truncateQueries) {
        execute(jdbcTemplate, "SET REFERENTIAL_INTEGRITY FALSE");
        truncateQueries.forEach(v -> execute(jdbcTemplate, v));
        execute(jdbcTemplate, "SET REFERENTIAL_INTEGRITY TRUE");
    }

    private void execute(final JdbcTemplate jdbcTemplate, final String query) {
        jdbcTemplate.execute(query);
    }
}

H2 인메모리 DB를 사용할 때 위와 같이 쿼리문을 작성한 메소드를 만듭니다.

AcceptanceTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Retention(RetentionPolicy.RUNTIME)
@TestExecutionListeners(value = {AcceptanceTestExecutionListener.class,},
        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public @interface AcceptanceTest {
}

여러 메타 에노테이션을 섞은 합성 에노테이션을 만듭니다.

 

@AcceptanceTest
public class ServiceTest {

	...
}

테스트를 작성할 때 AcceptanceTest 에노테이션을 사용하면 됩니다.

 

이렇게하면 @Transactional을 사용할 때의 문제점도 해결하고 @BeforeEach를 사용하여

직접 deleteAll 할 때 외래키문제로 오류가 발생하는 문제도 해결하여 독립적인 테스트를 

작성할 수 있습니다!!

추가

테스트 코드에서 @Trasactional 사용에 대한 논쟁에 대해 정리하고 제가 사용하는 방법을 정리해보았습니다.

 

테스트 코드에서 @Transactional 사용에 대한 끊임 없는 논쟁

각종 블로그 글, 유튜브, 페이스북, 오픈 카톡방, 인프런 글 등을 보다 보면 테스트 코드 작성시 @Transactional의 사용 여부는 끊임없이 논쟁되는 주제 같습니다. 여러 글들을 많이 보았는데 향로님

320hwany.tistory.com