본문 바로가기
회고록/Project

[42Blind] Service 계층 통합 테스트

by yhames 2024. 2. 25.
728x90

 

이 글은 NestJS를 사용하면서 Service 계층 통합 테스트에 대한 고민을 다룬 글입니다.

아직 이해가 부족한 부분이 많기 때문에 더 좋은 방향을 알려주시면 큰 도움이 될 것 같습니다.

 

한 줄 요약

SQLite In-memory DB를 활용한 통합 테스트를 진행하기로 결정했습니다.

Github Action을 활용한 e2e 테스트에서 실제 배포 환경과 동일하게 구성하여 진행하고,

필요시 Testcontainers를 도입하거나 단위테스트로 분리하는 리펙토링을 진행하기로 결정했습니다.

 

Service 계층 테스트 문제점

처음 테스트를 작성할 때, 모든 의존성을 Mock/Stub를 사용해서 단위 테스트를 작성하는 방식으로 접근했습니다.

하지만 현재 비즈니스 로직은 단위 테스트를 진행할 만큼 복잡하지 않았고, Jest에서 Mock/Stub 객체를 작성하는 것이 생각보다 쉽지 않았습니다.

 

아직까지는 비즈니스 로직이 단순하기 때문에, 단위 테스트보다는 DB를 활용한 통합 테스트가 우선순위가 높다고 판단하여 Service 계층에서 테스트를 하는 방법에는 어떤 것들이 있는지 알아보았습니다.

 

1. 테스트 서버(AWS 등)의 DB를 활용

저희 프로젝트의 경우에는 프론트와의 원활한 협업을 위해 API 명세를 문서화하여 테스트 서버에서 볼 수 있도록 구성했습니다.

 

테스트 서버의 DB를 활용한다면 실제 배포 환경과 가상 유사한 환경에서 테스트를 진행할 수 있고,

테스트 서버가 이미 구성되어있기 때문에 추가적인 설정을 하지 않아도 된다는 장점이 있었습니다.

 

하지만 테스트 서버의 DB를 사용한다면 테스트를 위한 더미 데이터가 지속적으로 축적이 되고

각 테스트가 독립적으로 실행되지 않는다는 단점이 있습니다.

이를 멱등성이라고 하며, 멱등성을 보장하기 위해서는 테스트 이후 더미 데이터를 명시적으로 초기화해야합니다.
예를 들어 UsersModule 테스트를 위해 user1이라는 데이터를 저장을 하고 더미 데이터로 남아있다면,
BoardsModule 테스트를 위해 user를 생성하거나 조회할 때 user1이라는 더미 데이터로 인해 예상과 다르게 동작할 수 있습니다.

 

멱등성을 보장하기 위한 방법으로 테스트 초기화 시 트랜잭션을 사용하고, 테스트 이후에 롤백을 하는 방법이 있습니다.

트랜잭션-롤백 테스트를 직접 경험해 본 것이 아니기 때문에 제가 참고했던 링크를 첨부하겠습니다.

테스트 데이터 초기화에 @Transactional 사용하는 것에 대한 생각
테스트에서 @Transactional 을 사용해야 할까?
토비님의 페이스북

 

트랜잭션-롤백 테스트는 분명히 유용한 방법이지만

저희 프로젝트 팀에서 아직 트랜잭션에 대한 이해가 부족하기 때문에 도입하지 않았습니다.

 

2. 컨테이너 환경의 DB 서버를 활용

실제 배포 환경과 유사한 환경을 가져가면서 테스트가 독립적으로 실행될 수 있는 방법으로

Docker와 같은 컨테이너를 사용해서 DB 서버를 올리는 방법이 있습니다.

 

컨테이너를 사용한다면 테스트를 실행할 때 컨테이너를 새롭게 띄우면

매번 새로운 환경에서 테스트를 실행하기 때문에 멱등성이 보장될 수 있습니다.

 

저희 프로젝트는 Github Action을 도입하면서 docker-compose를 사용해서 DB와 서버를 구성했기 때문에

테스트 서버를 사용하는 것과 마찬가지로 추가적인 설정이 필요하지 않았습니다.

 

하지만 각 테스트 마다 컨테이너를 새로 띄워야하기 때문에 반복적인 테스트에 어려움이 있습니다.

또한 docker-compose와 같은 외부 설정파일을 따로 관리해야한다는 번거로움이 있습니다.

매번 컨테이너를 새로 띄우지 않는다면 테스트용 클라우드 DB 서버를 사용하는 것과 마찬가지로
트랜잭션-롤백을 사용하거나 명시적으로 데이터를 초기화해야합니다.

 

추가로 Testcontainers를 사용하면 외부 설정 파일을 사용하지 않고 컨테이너를 활용할 수 있습니다.

Testcontainers도 직접 경험해보지 않았기 때문에 참고했던 링크만 첨부하겠습니다.

What is Testcontainers, and why should you use it?
TestContainer 로 멱등성있는 integration test 환경 구축하기

 

컨테이너를 활용해서 테스트용 DB 서버를 사용하는 방법은

명시적으로 데이터를 초기화를 해야한다는 점에서 클라우드 테스트 서버를 사용하는 방법과 다르지 않아보였습니다.

오히려 통합테스트를 위한 docker-compose를 따로 관리해야한다는 번거로움만 추가되는 것 같아서 도입하지 않았습니다.

 

3. In-Memory DB(SQLite, H2 등)을 활용

SQLite나 H2 같은 In-Memory DB를 사용한다면 실제 배포환경과는 일부 다르지만, 테스트를 독립적으로 실행할 수 있습니다.

 

저희 프로젝트는 TypeORM을 사용하고 있기 때문에 DB에 대한 종속성이 크지 않습니다.

따라서 SQLite를 사용하여 각 테스트마다 새로운 In-Memory DB를 띄워 사용하면

컨테이너를 사용할 때보다 테스트 속도가 빠르고, 더미 데이터를 초기화하지 않아도 멱등성을 보장할 수 있습니다.

 

하지만 In-Memory DB는 특정 DB 에 종속적인 기능이나 복잡한 쿼리에 대해서는 테스트할 수 없다는 단점이 있습니다.

 

현재 저희 프로젝트는 단순한 CURD 기능만을 구현하기 때문에 DB 종속적인 기능이나 복잡한 쿼리를 사용하지 않습니다.

따라서 현재 상황에서는 SQLite를 사용해서 통합 테스트를 하는 것이 가장 적합하다고 생각했습니다.

이후에 Service 계층에 복잡한 비즈니스 로직이 추가되거나, 복잡한 쿼리 등의 문제로 한계에 직면하면

컨테이너를 활용하거나 Service 계층 테스트를 분리하는 등의 리펙토링을 진행하는 것으로 결정했습니다.

 

추가로 DB 종속적인 기능을 테스트하기 위해 Embedded MySQL과 같은 임베디드 데이터베이스를 활용하는 방법이 있습니다.

이 또한 직접 경험하지 않아서 링크만 남기겠습니다.

wix-embedded-mysql

 

4. Mock/Stub 객체를 활용

마지막으로 통합 테스트는 아니지만 Service 계층에 복잡한 비즈니스 로직이 추가되거나

테스트 양이 많아져서 테스트 속도가 오래 걸린다면 Mock이나 Stub를 활용한 단위 테스트를 고려할 수 있습니다.

 

Mock과 Stub를 이해하고 좋은 단위테스트를 작성하기 위해서는

Test Double이 무엇인지 그리고 classical TDD  mockist TDD의 차이가 무엇인지 알고 있어야 합니다.

단위 테스트에 대해 깊게 다루는 것은 주제에 벗어나므로 다른 글에서 따로 다루겠습니다.

Mocks Aren't Stubs
Mocks Aren't Stubs - 번역 (Jeremy's Blog)

 

결론

처음에는 단위 테스트에 대한 환상을 가지고 어떻게든 Mock/Stub으로 해결하려고 했었습니다.

하지만 Service 계층의 다양한 테스트 방법들을 알아보면서 신뢰성 있는 테스트를 작성하기 위해서는 

실제 배포환경과 유사한 환경의 통합 테스트와 각 레이어 계층이 상호작용하는 e2e 테스트가 반드시 필요하다는 생각이 들었습니다.

 

또한 배포환경과의 유사성, 멱등성, 테스트 속도 등 통합 테스트를 하면서 고려해야하는 요소들을 알아보았고,

프로젝트를 진행하면서 발생하는 문제와 상황에 맞는 유연한 대처가 중요하다고 생각했습니다.

반응형