Test를 진행하는 여러가지 방식과 방법론에 대한 논제는 끊임없이 나오지만, Test에 대한 중요성은 누구도 부정할 수 없을거라고 생각합니다. 특히 데이터를 처리하는 코드를 작성하는 데이터 엔지니어에는 더욱 test가 중요할 수 밖에 없습니다. 그 이유는 크게 두가지가 있는 데, 다음과 같습니다.
데이터 엔지니어는 data pipeline을 효율적으로 구성해야 하는 일이 많다보니, 여러 시스템을 이어서 만드는 경우가 많습니다. 이 때문에 unit test로서는 모든 부분을 커버 하기 힘들어서, integration test를 통해 data가 다른 시스템으로 잘 넘어가는지 테스트를 자주 했습니다. 기존에 SQLDB로 이어지는 integration test를 임의로 H2DB (in-memory) 사용하여, SQLDB인척 많이 사용 했지만, SQLDB 종류로 많으며, data pipeline 성능을 끌어올리기 위해 해당 SQLDB 전용 기능들을 많이 사용하게 되면서, H2에서 지원하지 않는 기능들이 많아서 어디 누가 컨테이너 기반으로 test할 수 있게 해주는 라이브러리 없나 하던중에 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 library는 많은 annotation이 없어서 learning curve가 낮은거 같습니다. 기억해야 하는 annotation은 아래 두가지 밖에 없습니다.
@Testcontainers - JUnit5 에 extension인 Jupiter를 사용하려면, Test Class에 해당 annotation을 선언해야 제대로 작동을 한다.
@Container - Docker container를 변수로 할당한 객체에 선언해야 하는 annotation. 해당 annotation이 선언이 되어야, docker container에 life-cycle이 binding 된다.
Testcontainer 에서는 크게 3가지 방법에 컨테이너 사용법이 존재 하는데 아래와 같습니다.
private MySQLContainer<?> mysql = new MySQLContainer<>("mysql:5.7") // Restarted container
private static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:5.7") // Shared container
초반에 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); // 확인
}
}
데이터 엔지니어링에서 테스트의 중요성, 테스트 구성 시 유의해야할 점을 알 수 있어 좋았습니다. 유익한 글 감사합니다.^^