ecsimsw

통합 테스트 환경 구성, @Transactional을 붙여 롤백해도 괜찮을까 본문

통합 테스트 환경 구성, @Transactional을 붙여 롤백해도 괜찮을까

JinHwan Kim 2024. 5. 31. 08:27

통합 테스트 환경 구성

0. 유닛테스트와 통합테스트

 

PicUp에선 가급적 Test를 위한 Application context 를 지양했다. 유닛 테스트로 테스트 환경을 구성하는 시간을 줄이고 각 테스트를 짧게 하려고 노력했던 것 같다. 예를 들면 Controller는 standalone mock mvc로 api 의 요청, 응답 형태만 확인했고, Service Unit test는 테스트 외의 Service를 Mocking 하여 의도한 흐름대로 메서드가 호출되는지 정도만 확인했다.

 

통합테스트를 최대한 지양했지만 역설적으로 가장 크게 안정감을 얻고 있는 테스트는 통합테스트다. 특히 트랜잭션 처리가 의도한대로 동작하는지, 예외 처리 끝에 데이터가 어떻게 남는지 확인하는 등 가장 헷갈리고 놓치기 쉬운 포인트는 결국 통합테스트에서 확인하게 되는 것 같다.

 

그래서 통합테스트를 1. 정말 궁금한 포인트에, 2.가급적 작은 범위로, 3. 커버리지에 너무 집착하지 말고 라는 규칙을 만들어 작성 중이다. 큰 흐름을 확인할 수 있는 안정감은 좋지만 너무 집착하게되면 테스트 수행 시간 뿐 아니라, 코드를 작성과 유지 보수에 시간이 늘어 배보다 배꼽이 더 커지는 꼴을 피하고 싶었다.

 

1. 통합 테스트에서 외부 환경 의존을 제거한다.

 

통합 테스트 환경 구성에 외부 툴은 어떻게 처리하면 좋을까. Mockito로 서비스 전체의 동작을 통제할 수도 있을 것이고, Embedded나 Container로 툴을 띄워 동작을 실제로 확인해도 좋을 것 같다. Mockito 를 사용한 동작 통제는 반대로 동작을 흉내내는 코드 작성이 번거롭고 유지 보수에 불편할 수 있다. Embedded 방식은 배포 리소스 사용량에, Test 용 container 방식은 Container 가 실행이 가능한 환경 여부에 의존해야 한다는 불편함이 있는 것 같다.

 

예를 들어 PicUp에서는 Embedded 로 Redis 를 띄워 통합 테스트 환경을 구성했다. Redis 는 분산락이 제대로 동작하고 있는지 여부를 확인하는 테스트에서 Mocking 보다는 실제로 Redis 동작을 확인하고 싶었고, Container 실행 가능 환경 여부와 상관없이  테스트 수행이 가능했으면 했다. 

 

codemonstur/embedded-redis 를 사용했다. 실제로 포트를 점유해 Redis를 띄우기 때문에 해당 포트를 점유할 수 있도록 해야하고, 테스트를 마치면 Redis 서버를 종료해주어야 한다. 서버가 실행되고 종료되는데 걸리는 시간에 주의가 필요하다. Picup에서는 Spring test container 와 Test 용 Redis 서버의 생명 주기를 같이 하였고, Spring test container cache를 최대한 활용해서 레디스가 실행되고 종료되는데 걸리는 시간을 최소화하였다. 이 글을 쓰는 지금 시점에선 딱 한번 실행되고 종료된다.

 

덕분에 외부 환경 구성이 불필요한 테스트 코드를 만들 수 있었다. 특히 CI/CD에서 테스트를 위한 다른 외부 환경 구성이 필요없어져 편하다.

 

2. Spring test container cache 를 개선한다.

 

Spring test container cache 를 최대한 활용하기 위해 고민한다. 가장 먼저 테스트에 정말 Spring container 가 필요한 상황인지 고민한다. 예를 들어 WebMvcTest 을 MockMvcBuilders.standaloneSetup() 으로 대신할 수 있다면 Container 사용을 피하면서 Controller, Advice, Resolver, Interceptor 동작을 확인할 수 있다. 

 

최대한 Test 실행에 공통 Bean 을 갖도록 하여 Container 캐시를 사용할 수 있다. 특히 테스트마다 달리 지정된 MockBean 은 테스트에 필요한 Bean 을 달라 Cache를 방해하므로 주의한다. `logging.level.org.springframework.test.context.cache=DEBUG` 으로 cache 사용 정도를 로그로 확인할 수 있다.

 

Test application context 사용 최소화, 118 개의 테스트, 1sec 292ms

전체 2개의 TestContainer 사용한다.

 

 

테스트 멱등성과 @Transactional

이번 이슈는 `@SpringBootTest`에서 `@Transactional`을 붙여 롤백을 처리해도 괜찮을까를 고민하다가 처리 방안들을 정리하고 싶어서 만들게 되었다. 편한 롤백 때문에 자연스럽게 `@Transactional`을 붙여왔지만, 트랜잭션 처리와 예외 처리를 확인하는 테스트에 붙는 트랜잭션은 위험하다.

 

1. 트랜잭션 경계에 있는 테스트만 `@Transactional` 을 제거하고 직접 TearDown한다.

2. 매 테스트마다 Truncate 쿼리 or DeleteAll 쿼리로 데이터를 남기지 않도록 한다.

 

1번은 트랜잭션이 붙는 테스트와 아닌 테스트가 나뉘기 때문에 코드 통일이 안되고, 그 여부도 헷갈리기 쉬울 것 같다. 2번은 테스트용 데이터가 필요한 상황에서 치명적일 것 같다. 단순히 롤백이 Truncate 나 DeleteAll 이 아닌, 특정 상황을 매번 만들고 테스트해야 하는 상황이라면 그 특정 상황으로 만들기 위한 TearDown 품이 더 들지 않을까. 지금은 통합 테스트에 필요한 특정 상황이 크지 않다. 일단 2번으로 매 테스트마다 모든 데이터를 날리는 방식으로 테스트 멱등을 만드려고 한다.

 

만약 BeforeEach, AfterEach 로 상황을 만들고, 날리는 비용이 아깝다면, 모든 각 테스트의 불필요한 상황이 아닌지를 확인해볼 것 같다. 물론 각 테스트마다 멱등성이 정확하게 만족하면 가장 좋겠지만, 그게 불가능한 상황이라면 initialize 와 tearDown 이 정말 각 테스트마다 필수적인지 확인하고 테스트 환경 구간을 나눠 BeforeAll과 AfterAll을 사용해 구간별로만 멱등을 만드는 것도 테스트마다 초기화하는 것 대비 비용 절감이 클 것 같다.

 

예를 들어 아래처럼 `@BeforeAll`, `@AfterAll` + static method 를 사용하거나,`@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)` 으로 아예 테스트 생명 주기를 나눌 수도 있겠다.

 

Comments