테스트 컨테이너란 테스트를 위한 Database 환경을 완전히 분리하는 것을 의미하고 있어요.
먼저 왜 필요한지 알아보도록 하겠습니다.
테스트에서 Repository, Service, Controller 등 여러 Layer에 걸쳐서 테스트를 진행하는 것을 통합테스트라고 합니다.
이러한 통합테스트는 프로그램이 기대한대로 동작함을 보장해줄 수 있고, 또 요구사항의 변경에도 어느정도의 프로그램 안정성을 보장해줄 수 있습니다.
통합테스트는 이러한 이유로 진행하는데요, 테스트 실행과 관련해서 Flow를 먼저 소개하도록 하겠습니다.
먼저 e2e 테스트(통합 테스트 중 하나)를 진행하게 된다면, 기본적인 Flow는 해당 내용과 동일하거나 비슷하게 흘러가게 될 것입니다.
그런데 e2e 테스트를 하게 되면 고민이 될 수 있는 부분이 테스트 시, DB를 어떻게 연결할 것인가? 라는 부분입니다.
여러 방법이 있고 이를 이용해서 연결해도 되지만, 적어놓은 것과 같이 어디에 구성하더라도 고려해야하는 문제점이 있습니다. 특히, 동시에 테스트를 못한다는 점과, 외부 환경에 의존한다는 점이 통합테스트를 하는데 어렵게 만드는 이유이기도 합니다.
그래서 Test Container와 같은 개념이 나오게 됩니다.
간단하게, 필요한 Database를 테스트 시작 전 Docker의 Container로 띄우고, 테스트가 끝나면 삭제하는 방식으로 동작합니다.
이렇게 하게 되면, CICD 환경에서도 통합테스트를 수행할 수 있고, 테스트마다 독립적인 DB를 가지게되고, 더 이상 외부의 DB에 의존하지 않아도 되겠죠.
사용하게 되는 Flow는 다음과 같습니다.
이러한 Test Conatiner를 사용하게 되면 사용자는 더 이상 테스트를 위한 DB를 직접 구축할 필요가 없게 됩니다.
만세😀
구체적으로 적어보자면, 다음과 같습니다.
여러 곳에서 e2e test가 가능하다.
Test를 위한 DB를 따로 설정한 경우에는 여러 곳에서 e2e test를 돌리려고 하면, db의 data가 중간에 꼬여서 실패할 수 있었습니다. 이제는 어디서 테스트를 하는지에 상관없이 테스트를 진행할 수 있게 되었습니다.
CICD 진행 시, e2e test를 절차로 추가하고 실행됨을 보장할 수 있다.
외부의 DB가 있는 경우에는 e2e test를 할 수 있지만, 외부 DB의 상태에 따라 실패할 수 있습니다. 하지만 이제 CICD 진행하면서 파이프라인에서 Docker를 이용해 DB를 생성함에 따라, 외부의 DB를 더 이상 의존하지 않아도 되고 이를 통해서 실행됨을 보장할 수 있습니다.
Test를 위한 DB를 관리하지 않아도 된다.
외부 DB를 사용하게 되면 신경써야하는 부분이 늘어납니다. 쓸 수 있도록 인바운드포트 개방부터, 클라우드라면 비용도 고려해야 합니다. 이런 부분들은 사실 관리포인트가 늘어나는 것이라서 없는편이 좋겠죠?
Test Container는 공식 문서도 있고 여러 프레임워크에서 사용할 수 있도록 지원해주고 있습니다.
제 경우에는 Nest.js로 프로젝트를 구성하고 이를 수행하도록 코드를 작성해보도록 하겠습니다.
ORM은 Prisma를 사용하도록 하겠습니다.
RDBMS는 Postgres를 사용하겠습니다.
먼저 테스트 컨테이너를 이용하기 위해 필요한 라이브러리들을 설치합니다.
제 경우 pnpm을 사용하고 있어 해당 패키지 메니저를 통해서 필요한 라이브러리를 가져오겠습니다.
// 테스트에 필요한 의존성 추가
pnpm i -D supertest @types/supertest jest @types/jest
// 테스트 컨테이너 사용을 위한 의존성 추가
pnpm i -D testcontainers @testcontainers/postgresql
Prisma를 이용하게 된다면, 데이터베이스의 컨테이너에 우리가 만든 model을 기준으로 마이그레이션을 진행해야 합니다.
이러한 절차 진행을 위해, util 함수로 container를 시작하고 해당 컨테이너에 prismaService를 시작하는 부분을 작성하겠습니다.
테스트 컨테이너에 PrismaService를 생성하도록 하는 util 함수를 작성합니다.
- test.help.ts
...
const execAsync = promisify(exec);
/**
* postgresql testContainer Starter
*/
export const psqlTestContainerStarter = async(): Promise<{
container: StartedPostgreSqlContainer,
service: PrismaService
}> => {
const container = await new PostgreSqlContainer().start();
const config = {
host: container.getHost(),
port: container.getMappedPort(5432),
database: container.getDatabase(),
user: container.getUsername(),
password: container.getPassword(),
};
// Container 가 가지는 db 주소를 반환
const databaseUrl = `postgresql://${config.user}:${config.password}@${config.host}:${config.port}/${config.database}`;
// 스크립트 실행을 통해 DB Container에 우리가 지정한 prisma model로 migrate 진행
await execAsync(
`DATABASE_URL=${databaseUrl} npx prisma migrate deploy --preview-feature`
);
const service = new PrismaService({
datasources: {
db: {
url: databaseUrl,
},
},
});
return {
container,
service,
};
}
e2e test를 할때, 이렇게 생성한 PrismaService를 주입합니다.
(app.e2e-sepc.ts)
...
beforeAll(async () => {
// DB Conatiner와 연결된 prismaService 생성
const psqlConfig = await psqlTestContainerStarter();
postgresContainer = psqlConfig.container;
prismaService = psqlConfig.service;
// 테스트를 시작할 때, Test Container를 사용하는 PrismaService를 주입받음
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule,],
})
.overrideProvider(PrismaService)
.useValue(prismaService)
.compile();
jwtService = module.get<JwtService>(JwtService);
app = module.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
...
그리고 e2e test를 실행하면 ...
지금과 같이 Test Conatiner를 이용해서 e2e test가 진행되는 것을 볼 수 있습니다. 😀
Node진영 TestConatiner 자료 부족, 하지만 찾아보면 나오긴 함.
Test Conatiner와 관련된 자료는 Spring에서는 쉽게 찾아볼 수 있었는데 Nest.js에서는 많이 찾아볼 수 없었습니다.
찾더라도 외국 자료이고, 생략된 부분이 많아서 그대로 적용하기는 힘들었습니다. 특히 Prisma의 경우는 마이그레이션을 해서 컨테이너에 똑같은 DB환경을 구축해야 했는데, 이것을 찾는데 시간이 많이 소요되었습니다.
그래도, TestContainers 공식문서에서 Node를 이용한 연결 방법을 제공해주기도 했고, Prisma를 사용하는 Yarsa Labs 개발 블로그에서 PrismaService를 주입하는 방법에 대해서 도움을 얻었습니다.
처음 실행 시, TestContainer 실행을 위한 이미지 내려받는 절차 존재
처음 실행해보면 Docker에서 이미지를 받아오다보니 시작이 조금 걸리게 됩니다. 내 경우에는 테스트 중 시타임아웃이 발생해서 문제가 있는줄 알았는데, testcontainer를 위한 docker image를 받아오는데 시간이 걸렸던 것이었습니다. ... 🥹 이후에는 받아온 이미지를 바로 도커 컨테이너로 띄워서 문제가 없이 동작하게 되었습니다.
Test Container 위한 두개의 이미지
TestContainer를 수행할 때, 우리가 사용하는 DB Container 말고도 testcontainers/ryuk
이라는 이미지가 같이 오게 됩니다.
이 이미지는 테스트를 진행하고 나서, 테스트 컨테이너를 없애거나 삭제하는 것을 돕는 이미지입니다. :) 테스트 환경에서 여러 컨테이너를 생성하고, 이를 다시 삭제하게 될때, 이러한 이미지를 사용해서 삭제합니다.
테스트 컨테이너가 만능의 도구인 것처럼 위에 설명했지만, 테스트 컨테이너도 분명히 고려해야 할 점들이 존재합니다.
테스트하는 환경의 Docker가 무조건 설치되어 있어야 합니다.
Docker Container로 돌아가다보니, 테스트를 진행하는 환경에 필수적으로 도커가 설치되어야 합니다.
테스트 진행 시점에, 필요한 이미지를 다운받는 절차가 있습니다. -> 네트워크 부하 존재
제 경우에도 테스트를 진행하면 초기에 몇번 시간초과 이벤트가 발생했었습니다.
Docker Image를 받아오는데 걸리는 시간이 테스트 환경 구성에 들어가므로 해당 시간을 초과했고, 이를 해결하기 위해 환경 구성에 시간을 더 주는 방향으로 구성했습니다.
만약 CICD에서 통합테스트를 구성하게 된다면, CICD 환경에서도 도커 이미지를 받아오는 과정이 필요할 것이고 이는 네트워크 부하가 있음을 의미합니다.
충분한 시간이 주어지지 않는다면 테스트 환경 구성에 대한 시간 초과가 발생해서 테스트가 실패할 수 있습니다.
테스트 환경에서 Container 사용으로 인한 리소스 필요 -> 시스템 리소스 부하 존재
테스트 환경에서 컨테이너를 띄운다는 것은 추가적인 리소스를 필요로 합니다.
만약 테스트 환경에서 이를 감당할만큼 충분한 리소스가 확보되지 않는다면, 테스트 시간 증가, 또는 테스트 실패로 이루어질 수 있습니다.
따라서 ...
테스트 컨테이너를 사용하려면, 충분한 네트워크 부하를 감당할만하고 테스트를 진행할만큼 리소스가 충분해야 합니다.
지마켓 기술블로그 - Testcontainers로 통합테스트 만들기
딜리셔스 기술블로그 - Testcontainers를 이용한 테스트 환경 구축하기