본문 바로가기

Spring

@DirtiesContext를 사용하지 않고 테스트 데이터베이스 초기화하기

수많은 통합 테스트들을 작성하다 보면 여러 테스트 메서드들마다 테스트 데이터베이스에 다양한 데이터들을 저장하게 된다. 테스트 메서드들마다 어떤 데이터들을 저장하고 삭제했는지 신경쓰면서 구현할 수도 있지만 어떤 테스트가 다른 테스트에 영향을 받는 것은 좋지 않고, 무엇보다 테스트간 순서가 보장이 안 돼서 오히려 더 어렵다. 그래서 간편하게 @DirtiestContext를 사용하여 다음과 같은 방식을 사용한다.

@ActiveProfiles("test")
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IntegrationTest {
    @LocalServerPort
    private int port;

    @BeforeEach
    void setUp() {
        RestAssured.port = port;
    }
}

 

하지만 @DirtestContext는 모든 테스트 메서드가 실행될 때마다 Application Context를 재구성하기 때문에 비용적으로 큰 부담이 될 수 있다. 테스트 코드가 별로 없다면 모르겠지만 수백, 수천 개가 된다면 @DirtestContext를 사용하는 건 그다지 좋지 않은 방식이다. 대신 다음과 같이 자동으로 database를 초기화하는 클래스를 작성해둠으로써 많은 시간과 비용을 절약할 수 있다.

@Component
@ActiveProfiles("test")
@Transactional
public class DatabaseCleaner implements InitializingBean {

	// 프로젝트에서 생성한 table 이름을 상수로 저장
    private static final List<String> generatedTableNames = List.of("MEMBER", "MEMBER_PROFILE",
            "MEMBER_PROFILE_IMAGE", "ROADMAP", "ROADMAP_CATEGORY", "ROADMAP_CONTENT", "ROADMAP_NODE",
            "ROADMAP_NODE_IMAGE");

    @PersistenceContext
    private EntityManager entityManager;

    private List<String> tableNames;

    @Override
    public void afterPropertiesSet() {
        final Session session = entityManager.unwrap(Session.class);
        session.doWork(this::extractTableNames);
    }

    private void extractTableNames(final Connection conn) throws SQLException {
        final List<String> tableNames = new ArrayList<>();

        final ResultSet tables = conn.getMetaData()
                .getTables(conn.getCatalog(), null, "%", new String[]{"TABLE"});

        while (tables.next()) {
            tableNames.add(tables.getString("table_name"));
        }

		// 기존 코드 : this.taleNames = tableNames;
        this.tableNames = generatedTableNames;
    }

    public void execute() {
        final Session session = entityManager.unwrap(Session.class);
        session.doWork(this::cleanUpDatabase);
    }

    private void cleanUpDatabase(final Connection conn) throws SQLException {
        final Statement statement = conn.createStatement();
        statement.executeUpdate("SET REFERENTIAL_INTEGRITY FALSE");

        for (final String tableName : tableNames) {
            statement.executeUpdate("TRUNCATE TABLE " + tableName);
            statement.executeUpdate("ALTER TABLE " + tableName + " ALTER COLUMN id RESTART WITH 1");
        }

        statement.executeUpdate("SET REFERENTIAL_INTEGRITY TRUE");
    }
}
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestConstructor(autowireMode = AutowireMode.ALL)
public class IntegrationTest {

    @LocalServerPort
    protected int port;

    @Autowired
    protected ObjectMapper objectMapper;

    @Autowired
    private DatabaseCleaner databaseCleaner;

    @BeforeEach
    void setUp() {
        RestAssured.port = port;
    }

    @AfterEach
    void tearDown() {
        databaseCleaner.execute();
    }
}

https://bperhaps.tistory.com/entry/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%97%AC%ED%96%89%EA%B8%B0-2

 

테스트 코드 최적화 여행기 (2)

안녕하세요 깃들다팀의 손너잘 입니다. 테스트 최적화의 두번째 글을 작성하게 되었습니다. 이전 글에서 작성하였듯, 첫번째로 할 것은 DirtiesContext의 제거입니다. 많은 프로젝트에서 매번 테스

bperhaps.tistory.com

다음 글을 참조했지만 테스트에서 사용한 데이터베이스인 h2에 default 테이블값들이 등록되어있어서 제대로 실행이 되지 않았다. 그래서 추가적으로 generatedTableNames라는 리스트에 직접 만든 테이블 이름들을 따로 저장해두는 방식으로 구현하니 잘 작동하였다. 매번 저 리스트에 내용을 추가하는 것이 귀찮다면 사용하는 데이터베이스의 default 테이블 이름들을 따로 저장해둔 뒤 그 이름들을 제외하는 식으로 해도 좋을 것 같다. (우테코 크루 져니의 아이디어)