Integration Test Using TestContainers

Spring Boot 와 RDBMS 환경에서 개발하면서 Test 를 작성할 때, 테스트 환경의 경우 주로 H2 를 사용한다. H2 는 몇 줄만 추가해주면 Test 환경에서 사용이 가능하기 때문에 설정이 매우 쉽다. 하지만 production 환경에서는 MySQL, PostgreSQL, MS SQL, Oracle 등 H2 와는 다른 Database 를 사용하고 있다면 이슈가 발생할 수 있다.

기본적으로 지원되는 문법이 다르거나 Database Engine 이 동작하는 방식이 달라서 Test 를 신뢰할 수 없다. 이것은 본질적으로 Test Code 를 작성하는 의미를 지키기 힘들어진다.

H2 에서 compatible 모드를 지원하지만 본질적으로 완전하게 동일한 환경을 보장할 수 없다. 

아래 링크를 통해서 확인할 수 있다.

http://www.h2database.com/html/features.html#compatibility

모든 database engine 들은 조금씩 다르게 동작합니다. H2 는 ANSI 표준을 지원하고 다른 database 와 호환되도록 노력합니다. 하지만 여전히 차이가 존재합니다.

https://www.h2database.com/html/advanced.html#acid



또한, Transaction 도 다르게 동작한다. ACID 의 개념을 빠뜨릴 수 없는데 MySQL 의 경우 default Isolation Level 은 REPEATABLE READ 이지만 H2 default Isolation Level 은 READ COMMITED 이다. Phantom Read 이슈가 발생하게 된다. 

물론 설정을 REPEATABLE READ 로 변경하고 Production 환경과 최대한 비슷하게 설정하고나서 H2 사용을 고려해 볼 수 있다. 하지만, 쉽게 설정할 수 있다는 H2 의 장점이 없어진다.

따라서 Production 환경과 최대한 동일한 환경으로 쉽고 빠르게 설정하여 테스트를 진행할 수 있도록 TestContainers 를 사용하자.


TestContainers 는 Java library 이다. Integration test 를 복잡한 설정 없이 Docker container 를 통해서 빠르고 쉽게 할 수 있다. RDBMS 뿐만 아니라, Kafka, Nginx 등 Message Queue, Web Server 그리고 Custom Image 나아가 docker-compose 또한 사용할 수 있다.

그렇다면 이제 간단하게 기본 동작 방식과 간단한 팁을 예제를 통해서 확인해보자.

Local 개발환경은 docker compose 를 이용하여 MySQL 5.7 version 의 container 를 사용한다고 가정한다. 한글과 이모지를 사용할 수 있도록 character-set 과 collation 을 utf8mb4 로 설정해주고, init.sql 파일을 이용하여 처음 schema 와 기본 data 를 load 할 수 있도록하기 위해서 skip-character-set-client-handshake 커맨드를 사용했다.


Local 개발환경 profile yaml 파일이다.



기본적인 MySQL 기반 JPA Spring Boot Application 을 아래와 같이 만들었다.

그리고 testcontainers 의존성을 추가 해준다. junit-jupiter, mysql


Writer 1 : N Book 관계의 Entity 로 Book 에서 Writer 로 단방향 관계를 맺어주었다.

그리고 간단하게 Service, Repository 작성하였다.


아래와 같이 testcontainers 를 이용하여 Integration test code 를 작성했다. 



test 환경 profile yaml 파일이다.


Jupiter / Junit5  2 가지 동작 방식

- 모든 test method 마다 재시작되는 container
- 하나의 test class 의 모든 test method 를 공유하는 container

static 키워드를 사용한 MySQL Container 는 class 안의 모든 test method 는 해당 Container 를 공유할 수 있다.
반면에 static 키워드가 없는 PostgreSQL Container 는 test method 마다 다른 Container 가 생성된다.


따라서, 하나의 test method 가 시작되기전에 container 가 시작되고 test method 가 동작하고 종료되면 해당 container 도 종료된다. 따라서 test method 가 많아질수록 container boot time 이 증가된다.

Reuse 를 통한 컨테이너 부트 타임 줄이기

Container 를 생성할 때 withReuse(true) 를 통해서 shouldBeReused 필드를 설정하여 컨테이너 재사용 여부를 설정할 수 있다. 예제에서 나오는 MySQLContainer 는 GenericContainer 를 상속하고 있다. GenericContainer 의 tryStart 메서드를 보면 shouldBeReused 필드와 TestcontainersConfiguration.getInstance().environmentSupportsReuse() 메서드를 통해서 재사용 여부를 확인하고 가능하다면 컨테이너 hash 값을 통해서 재사용을 할 수 있도록 한다.


TestcontainersConfiguration.getInstance().environmentSupportsReuse() 를 보면 아래와 같이 testcontainers.reuse.enable 의 값을 읽어서 재사용 여부를 판단하고 있다.
결론은 withReuse(true) 를 통해서 설정해주고 이와 동시에 test code 를 실행하는 host machine 의 ~/.testcontainer.properties 파일안에 testcontainers.reuse.enable 값을 true 로 설정해주어야한다.

Singleton Container Pattern

서비스가 성장함에 따라 기능이 많아지게 되면 Integration test 양도 당연히 많아진다. 이는 class 숫자가 증가하게 된다. testcontainers 를 사용할 때 가장 유의할 점이 boot time 관리이다.

추상 클래스 활용

abstract class 를 작성하고 상속을 통해서 여러 class 가 오직 하나의 container 를 사용하도록 할 수 있다.



인터페이스 활용

상속의 가장 큰 문제점은 다이아몬드 상속 문제이다. 따라서 Java 에서는 다중상속을 할 수 없다. 이로인해 발생할 수 있는 문제점은 상속을 받은 자식은 부모의 모든 기능을 상속받지 않고 싶을 수 있다. 예를 들어, 부모 클래스에서 MySQL, Kafka, Nginx Container 를 관리하고 있다고 가정해보자. 자식 클래스는 MySQL, Kafka Container 만을 사용하여 Integration Test 진행해야 한다면 Nginx Container 는 낭비가 된다. 이러한 문제를 Interface 를 활용하여 해결할 수 있다.




마지막으로 테스트 성공 결과로 이글을 마무리하겠다.



 참고

https://github.com/ndgndg91/testcontainers-demo

http://www.h2database.com/html/features.html#compatibility

https://www.h2database.com/html/advanced.html#acid

https://www.testcontainers.org/

https://www.testcontainers.org/test_framework_integration/junit_5/

https://github.com/testcontainers/testcontainers-java/blob/master/core/src/main/java/org/testcontainers/containers/GenericContainer.java

https://github.com/testcontainers/testcontainers-java/blob/7b098bc6bbdf18cf1c212337ba3e39afef6212a5/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java#L167

https://github.com/testcontainers/testcontainers-java

https://www.youtube.com/watch?v=v3eQCIWLYOw

https://bsideup.github.io/posts/local_development_with_testcontainers/

https://devs0n.tistory.com/24

https://pawelpluta.com/optimise-testcontainers-for-better-tests-performance/

https://medium.com/pictet-technologies-blog/speeding-up-your-integration-tests-with-testcontainers-e54ab655c03d

댓글

이 블로그의 인기 게시물

Spring Boot Actuator readiness, liveness probes on k8s

About Kafka Basic

sneak peek jitpack

About idempotent

About G1 GC

About ZGC

About JVM Warm up

I need to know a little JVM

HackerRank Java Between Two Sets

Java - HashMap (feat. LinkedList, Tree.. maybe Later)