Testcontainers 사용 기초

Andy (Yoon Yong) Shin·2021년 8월 28일
1
post-thumbnail

Data Engineer에게 Test란

Test를 진행하는 여러가지 방식과 방법론에 대한 논제는 끊임없이 나오지만, Test에 대한 중요성은 누구도 부정할 수 없을거라고 생각합니다. 특히 데이터를 처리하는 코드를 작성하는 데이터 엔지니어에는 더욱 test가 중요할 수 밖에 없습니다. 그 이유는 크게 두가지가 있는 데, 다음과 같습니다.

  1. 웹사이트를 만들거나, 표면적으로 들어나는 결과물에 대한 코드는, 개발자 test 외에 비개발자가 실제 사용을 통해 test를 진행할 수 있기에, 상대적으로 적은 테스트로도 커버할 수 있습니다. 반대로 데이터 엔지니어가 작업한 코드 내용은 내부에 자리잡은 코드일 가능성이 높으며, 지속적으로 사용자에 의해 사용 되는 기능이 아니기에 버그를 놓치면, 버그가 있는 상태로 장기간 코드가 사용될 가능성이 아주 높습니다.
  2. 통상적으로 데이터 엔지니어가 처리한 데이터를 기반으로 통계 및 분석을 진행하게 되는데, 해당 데이터는 중요한 비즈니스 결정에 사용되는 경우가 많습니다. 그러므로, 데이터 엔지니어가 담당하는 데이터에 결함이 있다면, 해당 데이터를 사용하는 비즈니스에 큰 타격을 줍니다. 추가로, 데이터는 대부분에 시스템에 근간이 되는 요소이기 때문에, 해당 데이터를 사용하여, 만들어진 기능들이 모두 영향을 받습니다. 데이터 엔지니어로서는, test에 많은 시간을 투자해서라도, data quality와 data integrity를 사수 해야 합니다. (e.g. 데이터를 수집하는 파이프라인을 담당 하고 있는데, 데이터 로그 자체가 잘못 수집되었다면, 몇개월을 다시 수집해야 하는 최악에 상황도 경험해야 할수도 있습니다.)

Data Engineer에 Test환경

데이터 엔지니어는 data pipeline을 효율적으로 구성해야 하는 일이 많다보니, 여러 시스템을 이어서 만드는 경우가 많습니다. 이 때문에 unit test로서는 모든 부분을 커버 하기 힘들어서, integration test를 통해 data가 다른 시스템으로 잘 넘어가는지 테스트를 자주 했습니다. 기존에 SQLDB로 이어지는 integration test를 임의로 H2DB (in-memory) 사용하여, SQLDB인척 많이 사용 했지만, SQLDB 종류로 많으며, data pipeline 성능을 끌어올리기 위해 해당 SQLDB 전용 기능들을 많이 사용하게 되면서, H2에서 지원하지 않는 기능들이 많아서 어디 누가 컨테이너 기반으로 test할 수 있게 해주는 라이브러리 없나 하던중에 Testcontainer 찾게 되었습니다.

Testcontainer 소개

Testcontainer는 이름으로 바로 도구에 사용법을 유추할 수 있듯이, test를 구성하게 될때 mocking 말고 실제 환경과 동일하게 테스트할 수 있게 docker로 환경을 구성하여, integration test를 진행할 수 있게 만들어진 library입니다. Java library 기준으로 MIT License로 open source이라고 보실수 있으며, java이외에도 javascript (nodejs), golang, scala, python 그리고 rust를 지원하고 있습니다. API 구성은 generic하게 docker cli에 command들을 선택한 language로 실행하고 관리할 수 있으며, (volumn, netowrk 등 docker cli할 수 있는 건 다 구현된거 같습니다.) 추가로 test에 많이 사용되는 특정 docker image들은, 해당 docker image에 알맞게 module이라는 이름으로 추가 API가 제공 됩니다. (MySQL, Redis, PostSQL, Kafka 등 생각보다 바로 사용할수 있는 image 전용 module이 많습니다.) 현재로서는 Java testcontainer가 메인 인거 같습니다. 현재 2021/08/28 기준 github star가 5.2k(5200) 정도이며, java 메인 unit test library인 JUnit5가 4.7k(4700) 인것을 가정하면, 꽤나 높은 별점을 가진거 같습니다. (java 자체가 사실 특정 library 빼곤 gitstar가 평균적으로 다른 language에 비해 낮은 편인거 같긴해서… 제 기준에서는 그나마 사용해보기에 괜찮아 보였습니다. 2021/08/28 기준 Contributor도 296명이네요 ㅎ)

Testcontainer Java 사용법

Annotation

감사하게도 testcontainer library는 많은 annotation이 없어서 learning curve가 낮은거 같습니다. 기억해야 하는 annotation은 아래 두가지 밖에 없습니다.

@Testcontainers - JUnit5 에 extension인 Jupiter를 사용하려면, Test Class에 해당 annotation을 선언해야 제대로 작동을 한다.

@Container - Docker container를 변수로 할당한 객체에 선언해야 하는 annotation. 해당 annotation이 선언이 되어야, docker container에 life-cycle이 binding 된다.

Container 재사용

Testcontainer 에서는 크게 3가지 방법에 컨테이너 사용법이 존재 하는데 아래와 같습니다.

  1. Restarted container - 모든 test에 새롭게 docker container가 생성되어, 테스트간에 공유가 되지 않는다. 당연히 해당 방법을 채택한다면, 테스트가 느려집니다.
  2. Shared container - 모든 test에 1개의 docker container가 생성 됩니다. 빠르게 test 진행 되지만, 사실 테스트간에 depency를 최소화 하여, 개별로 test를 구동 가능하게 만드는 게 best practice라 저같은 경우 shared container를 사용 하지만, table을 분리 하거나, row를 분리하여, 테스트 데이터를 생성하여 test case를 만듭니다. 아래 사용 예제 기준으로 변수에 static인지 아닌지에 따라서, shared container 또는 restarted container로 실행이 됩니다.
    private MySQLContainer<?> mysql = new MySQLContainer<>("mysql:5.7") // Restarted container 
    private static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:5.7") // Shared container 
  1. Singleton containers - 이름과 동일하게 BaseTest를 만들고, 해당 base class를 상속받는 모든 test class에게 동일한 docker container를 사용 하는 방식이다. Test 숫자가 많으면, 해당 방식을 사용하여, test 속도를 많이 향상 시킬수 있습니다.

현재 이슈

초반에 Testcontainer가 만들어졌을때 JUnit 4를 염두하여, 개발이 되다 보니, JUnit4를 기준으로는 parallel test가 가능한데, 현재 JUnit5 with Jupiter에서는 문제가 있는 거 같습니다. 저 같은 경우 JUnit5 with Jupiter으로 일단 작업을 하고 해당 이슈를 tracking 하시면, 기다리고 있습니다...

사용 예제

아래는 간단한 사용예제 입니다.

@Slf4j
**@Testcontainers // Jupiter** extension 연동
class DTODefaultTest {

		**@Container // testcontainer docker life cycle 연결**
    private static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:5.7") // Mysql version 설정
            .withDatabaseName("test1") // DB 설정
            .withUsername("username") // ID 설정
            .withPassword("password2") // Password 설정
            .withInitScript("01_schema_data.sql") // Event table 만들고 event 5개를 삽입하는 script
            .withCommand("--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"); // utf8 encoding으로 한글 인식하게 설정변경

		private static HikariConfig config = new HikariConfig(); // connection pool
    private static HikariDataSource ds;
		private final JdbcTemplate jdbcTemplate; // jdbc template 사용

		@BeforeAll
    public static void beforeAll() {
        config.setJdbcUrl(mysql.getJdbcUrl()); // 위 MySQL 컨테이너에 사용되는 host 설정 docker에서 사용 되는 dynamic port도 포함
        config.setUsername(mysql.getUsername()); // 위 MySQL 컨테이너에 사용되는 user 설정
        config.setPassword(mysql.getPassword()); //  위 MySQL 컨테이너에 사용되는 password 설정
        ds = new HikariDataSource(config);
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Test
    @DisplayName("Read count test - should return 5 event records")
    void test1() {
				Collection<ResultSet> result = jdbcTemplate.query("select * from event",
                ((resultSet, i) -> resultSet)); // 이벤트 5개 회수
        assertThat(result.size()).isEqualTo(5); // 확인
    }
}

Reference

https://github.com/orgs/testcontainers/repositories

https://www.testcontainers.org/

1개의 댓글

comment-user-thumbnail
2021년 9월 5일

데이터 엔지니어링에서 테스트의 중요성, 테스트 구성 시 유의해야할 점을 알 수 있어 좋았습니다. 유익한 글 감사합니다.^^

답글 달기