NestJS를 기반으로 프로젝트를 진행하고 있는데, 핵심기능 테스트를 하기 위해 테스트에 잘못 발을 딛었다가 호되게 혼나고 거의 1달 동안 밤낮을 새가며 어느 정도 테스트 구조에 대해 갈피를 잡았다.
테스트를 효율적으로 운영하기 위해서는 단위(Unit) 테스트와 통합(Integration) 테스트를 명확하게 분리하는 것은 당연히 중요하고, Jest를 이용해서 얼마나 똑똑하게 실행할 것인지가 사실 관건이다. 공식문서에 가보면 Jest는 다양한 CLI 기능을 제공한다. Jest 공식문서
이 글에서는 실제 프로젝트에서 어떻게 테스트 환경을 분리하고, 실행 구조를 구성했는지에 대해 정리해 볼 것이다. (문제 해결과 관련된 이야기는 다음 글에서 시리즈로 다룰 예정)
테스트 전략
서버 전략은 다음과 같이 구성하고 들어가 봅시다. 단위, 통합, E2E 테스트가 존재하는데 각 테스트마다 목적이 다르다. 하지만 본질은 테스트라는 것에 명심하자. 결국은 원하는 동작을 하는지 검증하는행위이기때문에 너무 겁먹지 말자. 처음에는 테스트에 대해 거부감이 많이 들었었다... 일단 뭐든 해봐야지...
단위 테스트
단위 테스트는 mocking을 활용하여, 실제 서비스의 핵심 비즈니스 로직이 돌아가는 부분을 테스트하기 위해 mocking을 활용해 테스트를 위한 의존성들을 mock으로 대체하여, 순수하게 로직 자체만을 검증한다.
(mocking에 대해서는 강경파와 온건파가 나뉘는 것 같던데... 개인적으로는 서비스는 모킹하는 게 좋은 것 같다. 탈모 방지를 위해)
통합 테스트
Presentation 레이어 (Controller)는 실제 네트워크 요청을 날려, 해당 레이어에 맡은 책임들을 검사하고, 의도한 대로 작동하는지 (Validation, Parsing, Authorization 등)
Persistence (Repository) 레이어는 실제 DB(PostgreSQL, Redis)를 연결해서, 원하는 동작을 실제 쿼리문을 날리고, 원하는 결과값이 잘 저장되는지 검증한다. 이 부분에서는 Product Database와 동일한 환경을 Docker를 이용해서 구축함. (아래에서 docker-compose파일 있을거임 잠시대기)
여기서 중요한 점은 두 테스트는 완전히 별도의 실행 환경으로 분리해서 관리하는 것
이를 위해 Jest에서는 jest.config.js라는 별도의 config 파일을 제공하는데, 이걸 잘 사용하는 게 반은 먹고 들어간다.
시간 나면 Jest config 공식문서에서 정독은 아니더라도, "이 프로퍼티는 뭐지?" 싶은 건 봐두는 게 좋다.
현재 테스트 구조
실제 테스트 파일들은 다음과 같은 규칙으로 정의해놨다. 도메인 디렉토리의 __test__ 하위에 unit, integration 안에 **/**.spec.ts 필요하면 fixture도 따로 추가하면 좋다
export class TestFixtureUtil {
private static instance: TestFixtureUtil;
public prisma: PrismaClient;
public redis: Redis;
private initialized = false;
private testUser1: UserEntity;
private testUser2: UserEntity;
private constructor() {
this.prisma = new PrismaClient({
datasources: {
db: {
url: testConfiguration().database.url,
},
},
});
this.redis = new Redis({
host: testConfiguration().redis.host,
port: testConfiguration().redis.port,
});
}
static getInstance(): TestFixtureUtil {
if (!TestFixtureUtil.instance) {
TestFixtureUtil.instance = new TestFixtureUtil();
}
return TestFixtureUtil.instance;
}
async setUp() {
if (this.initialized) return;
await this.prisma.$connect();
this.initialized = true;
}
async createTestUser() {
// 시드 데이터 삽입
}
async resetAll() {
// 테스트 간 데이터 초기화
}
async tearDown() {
await this.prisma.$disconnect();
await this.redis.quit();
}
}
여기서 싱글톤으로 만든 이유는 테스트 실행 중 중복 인스턴스 생성을 방지하고, Prisma/Redis 커넥션을 테스트 전체에서 공유하도록 하기 위함
정리하며
지금까지 테스트 구조를 어떻게 구성했는지, 단위 테스트와 통합 테스트를 분리하고 독립적으로 실행되게 만드는 구조를 어떻게 잡았는지 정리해봤다. 테스트는 정말 연약한 존재이기때문에 (구조잘못짜면개고생) 초기작업을 잘 마쳐줘야 한다. 이후에는 픽스쳐를 얼마나 잘 활용하느냐에 따라서 테스트작성 속도와, 어플리케이션 무결성이 보장된다 리얼
하지만, 위에서 언급한 구조에서도 실제로 문제가 생긴다. 예를 들어:
setup.ts가 테스트마다 여러 번 실행되며 Redis 커넥션이 계속 열리고
테스트가 끝났는데도 Jest가 종료되지 않는 등
이 문제들을 가지고 삽질해서 또 풀어냈는데, 다음 글에서 설명해보도록 하겠습니다. 다음 글에서는 "setup.ts 반복 실행 이슈와 리소스 누수 문제를 어떻게 해결했는가" 에 대해 다룰 예정입니다
이 글은 기본설계잡는거에 초점을 뒀다면, 다음 글에서는 트러블슈팅에 대해서 작성할 예정 끗