테스트를 위한 데이터 베이스 초기화

김성재·2024년 3월 10일
0

전에 말한 듯이 있듯이, BCSD Lab에서 운영중인 서비스, 코인을 스프링3에서 스프링 부트로 마이그레이션 하는 작업을 우리는 하고 있다.

마이그레이션을 하고 push를 하기 전에 기존에 스프링3로 작성된 프로젝트랑 응답 반환 값이 똑같은지 인수테스트를 거쳐야 한다. 테스트를 할 때마다 데이터베이스에 이상한 값이 들어 있으면 안되니까 초기화 하는 과정을 거치는데 이번 글은 이 데이터 베이스를 초기화 하는 과정을 분석해보려 한다.

모든 테스트는 AcceptanceTest를 상속하고 있다 그 AcceptanceTest는 DBInitializer가 주입되어 있다.
DBInitializer부터 살펴보자

DBInitializer

@TestComponent
public class DBInitializer {

    private static final int OFF = 0;
    private static final int ON = 1;
    private static final int COLUMN_INDEX = 1;

    private final List<String> tableNames = new ArrayList<>();

    @Autowired
    private DataSource dataSource;

    @PersistenceContext
    private EntityManager entityManager;

    private void findDatabaseTableNames() {
        try (final Statement statement = dataSource.getConnection().createStatement()) {
            ResultSet resultSet = statement.executeQuery("SHOW TABLES");
            while (resultSet.next()) {
                final String tableName = resultSet.getString(COLUMN_INDEX);
                tableNames.add(tableName);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void truncate() {
        setForeignKeyCheck(OFF);
        for (String tableName : tableNames) {
            entityManager.createNativeQuery(String.format("TRUNCATE TABLE %s", tableName)).executeUpdate();
        }
        setForeignKeyCheck(ON);
    }

    private void setForeignKeyCheck(int mode) {
        entityManager.createNativeQuery(String.format("SET FOREIGN_KEY_CHECKS = %d", mode)).executeUpdate();
    }

    @Transactional
    public void clear() {
        if (tableNames.isEmpty()) {
            findDatabaseTableNames();
        }
        entityManager.clear();
        truncate();
    }
}

흐름을 따라 차례대로 살펴보자

  • @TestComponent: 스프링 부트 테스트 환경에서 사용될 컴포넌트임을 나타낸다. 테스트 시에만 활성화된다.

  • DataSource 및 EntityManager 주입: @Autowired와 @PersistenceContext(entity manager를 주입할 때 사용)를 통해 DataSource와 EntityManager를 주입받는다. 데이터베이스 연결과 쿼리 실행에 사용된다.

  • findDatabaseTableNames 메서드: 데이터베이스의 모든 테이블 이름을 조회(SHOW TABLES 쿼리문)하여 tableNames 리스트에 저장한다. 이 과정은 테이블을 트랜케이트하기 전에 필요한 정보를 수집하는 단계다.

  • truncate 메서드: 외래 키 제약 조건을 일시적으로 비활성화(SET FOREIGN_KEY_CHECKS = 0 쿼리문)한 후 모든 테이블을 (TRUNCATE TABLE) 쿼리문을 이용하여 데이터를 초기화한다. 이후 외래 키 제약을 다시 활성화한다.

  • clear 메소드: 외부에서 호출되어 실제 데이터베이스 초기화 과정을 수행한다. 테이블 이름이 미리 수집되지 않았다면 findDatabaseTableNames를 호출해 수집하고, entityManager.clear()로 캐시된 엔티티를 제거한 뒤 truncate를 호출하여 데이터베이스를 초기화한다.

AcceptanceTest

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Import(DBInitializer.class)
@ActiveProfiles("test")
public abstract class AcceptanceTest {

    private static final String ROOT = "test";
    private static final String ROOT_PASSWORD = "1234";

    @LocalServerPort
    protected int port;

    @Autowired
    private DBInitializer dataInitializer;

    @Container
    protected static MySQLContainer mySqlContainer;

    @Container
    protected static GenericContainer<?> redisContainer;

    @DynamicPropertySource
    private static void configureProperties(final DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mySqlContainer::getJdbcUrl);
        registry.add("spring.datasource.username", () -> ROOT);
        registry.add("spring.datasource.password", () -> ROOT_PASSWORD);
        registry.add("spring.data.redis.host", redisContainer::getHost);
        registry.add("spring.data.redis.port", () -> redisContainer.getMappedPort(6379).toString());
    }

    static {
        mySqlContainer = (MySQLContainer) new MySQLContainer("mysql:5.7.34")
            .withDatabaseName("test")
            .withUsername(ROOT)
            .withPassword(ROOT_PASSWORD)
            .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci");

        redisContainer = new GenericContainer<>(
            DockerImageName.parse("redis:4.0.10"))
            .withExposedPorts(6379);

        mySqlContainer.start();
        redisContainer.start();
    }

    @BeforeEach
    void delete() {
        if (RestAssured.port == RestAssured.UNDEFINED_PORT) {
            RestAssured.port = port;
        }
        dataInitializer.clear();
    }
}
  • @SpringBootTest: 스프링 부트 애플리케이션을 테스트 환경에서 실행한다. webEnvironment = RANDOM_PORT 설정은 내장된 웹 서버를 랜덤 포트에 오픈하여 테스트를 실행하게 한다. 이렇게 하면 실제 웹 서버가 실행되는 환경을 테스트할 수 있다.

  • @Import(DBInitializer.class): DBInitializer 클래스를 스프링 애플리케이션 컨텍스트에 추가한다.

  • @ActiveProfiles("test"): "test" 프로필을 활성화하여, 테스트 시 특정 설정을 사용하도록 한다.

  • @DynamicPropertySource: 테스트 실행 시 동적으로 속성 값을 설정한다. 이 어노테이션이 달려 있는 코드는 Testcontainers로부터 생성된 MySQL과 Redis 컨테이너의 연결 정보를 스프링 애플리케이션 설정에 주입한다.

  • @BeforeEach: 각 테스트 메서드가 실행되기 전에 이 어노테이션이 달린 메서드가 호출된다. 각 테스트가 독립적인 환경에서 실행될 수 있도록 보장한다.

이제 AcceptanceTest를 상속 받는 인수 테스트들은 테스트 메서드를 실행하기 전에 데이터 베이스 초기화가 자동으로 이뤄진다!

0개의 댓글