icon
8장 : 테스팅 전략

E2E 테스트 구현


지난 절에서는 NestJS에서 가장 기본적이고 빠른 피드백을 제공하는 단위 테스트를 작성하고, 의존성을 효과적으로 모킹(Mocking) 하는 방법을 배웠습니다. 이제 8장의 두 번째 절로, 사용자 관점에서 애플리케이션의 전체 흐름을 검증하는 E2E(End-to-End) 테스트를 NestJS에서 어떻게 구현하는지 알아보겠습니다.

단위 테스트가 개별 코드 조각의 정확성을 보장한다면, E2E 테스트는 여러 서비스, 데이터베이스, 네트워크 등 실제 환경에 가까운 조건에서 시스템 전체가 예상대로 작동하는지를 검증합니다. 이는 애플리케이션이 프로덕션 환경에서 사용자에게 어떤 경험을 제공할지 미리 확인하는 중요한 단계입니다.


E2E 테스트란?

E2E 테스트(End-to-End Test) 는 애플리케이션의 시작부터 끝까지의 전체 워크플로우를 시뮬레이션하여 시스템이 의도한 대로 동작하는지 검증하는 테스트 유형입니다. 웹 애플리케이션의 경우, 사용자가 웹 브라우저를 통해 서비스를 이용하는 것과 동일한 방식으로 테스트를 진행합니다.

E2E 테스트의 주요 특징

  • 사용자 관점: 실제 사용자가 겪을 수 있는 시나리오를 재현합니다.
  • 전체 시스템 검증: 프론트엔드, 백엔드, 데이터베이스, 외부 API 연동 등 시스템의 모든 구성 요소가 포함될 수 있습니다.
  • 통합된 동작 확인: 여러 컴포넌트가 올바르게 통합되어 작동하는지 확인합니다.
  • 느린 실행 속도: 실제 시스템을 구동해야 하므로 단위 테스트나 통합 테스트보다 실행 시간이 오래 걸립니다.
  • 취약한 안정성(Flakiness): 네트워크 지연, 외부 서비스의 상태, 비동기 작업 등으로 인해 불안정한 테스트(Flaky Test)가 발생할 수 있습니다.

NestJS에서 E2E 테스트의 역할

NestJS 백엔드 애플리케이션의 경우, E2E 테스트는 주로 HTTP/REST API 엔드포인트를 호출하여 서버의 응답을 검증하는 방식으로 진행됩니다. 프론트엔드가 있는 경우, Cypress나 Playwright 같은 도구를 함께 사용하여 웹 브라우저에서의 상호작용까지 포함할 수 있습니다. NestJS는 기본적으로 JestSupertest 라이브러리를 사용하여 E2E 테스트를 위한 환경을 제공합니다.


NestJS E2E 테스트 환경 설정

새 NestJS 프로젝트를 생성하면 test 폴더에 app.e2e-spec.ts 파일과 jest-e2e.json 설정 파일이 기본으로 제공됩니다.

jest-e2e.json (E2E 테스트를 위한 Jest 설정)

jest-e2e.json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$", // .e2e-spec.ts로 끝나는 파일만 테스트
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "moduleNameMapper": {
    "^src/(.*)$": "<rootDir>/src/$1" // src 경로 별칭 설정 (옵션)
  }
}
  • testRegex: .e2e-spec.ts로 끝나는 파일만 E2E 테스트로 인식하여 실행합니다. 이는 단위 테스트와 E2E 테스트를 분리하여 실행할 때 유용합니다.

E2E 테스트 작성: AppController 예시

AppController의 간단한 HTTP 엔드포인트를 테스트하는 예시를 통해 E2E 테스트의 기본 구조를 이해해 보겠습니다.

AppController 코드 (이전과 동일)

src/app.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('hello')
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('sum')
  sumNumbers(@Body() data: { a: number; b: number }): number {
    return this.appService.sum(data.a, data.b);
  }
}

AppService 코드 (이전과 동일)

src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }

  sum(a: number, b: number): number {
    return a + b;
  }
}

app.e2e-spec.ts 파일

NestJS가 기본으로 제공하는 app.e2e-spec.ts 파일을 수정하여 사용하거나, 새로운 E2E 테스트 파일을 생성할 수 있습니다.

test/app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest'; // Supertest 임포트
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication; // NestJS 애플리케이션 인스턴스

  // 각 테스트 스위트가 시작되기 전에 단 한 번 실행됩니다.
  // NestJS 애플리케이션을 초기화하고 HTTP 요청을 보낼 준비를 합니다.
  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule], // 전체 AppModule을 임포트하여 실제 모듈 구조를 사용
    }).compile();

    app = moduleFixture.createNestApplication(); // NestJS 애플리케이션 인스턴스 생성
    await app.init(); // 애플리케이션 초기화 (모든 모듈, 컨트롤러, 서비스 초기화)
  });

  // 각 테스트 스위트가 끝난 후 단 한 번 실행됩니다.
  // NestJS 애플리케이션을 종료하여 리소스를 정리합니다.
  afterAll(async () => {
    await app.close();
  });

  // --- GET /app/hello 엔드포인트 테스트 ---
  it('/app/hello (GET)', () => {
    return request(app.getHttpServer()) // Supertest를 사용하여 HTTP 서버에 요청
      .get('/app/hello') // GET 요청
      .expect(200) // HTTP 상태 코드 200 (OK) 예상
      .expect('Hello World!'); // 응답 본문이 'Hello World!'인지 예상
  });

  // --- POST /app/sum 엔드포인트 테스트 ---
  it('/app/sum (POST)', () => {
    return request(app.getHttpServer())
      .post('/app/sum') // POST 요청
      .send({ a: 5, b: 7 }) // 요청 본문 (JSON) 전송
      .expect(201) // HTTP 상태 코드 201 (Created) 예상 (NestJS는 기본적으로 POST에 201 반환)
      .expect('12'); // 응답 본문이 '12' (문자열로 반환됨)인지 예상
  });

  // --- 존재하지 않는 경로 테스트 (404 Not Found) ---
  it('/non-existent-path (GET) should return 404', () => {
    return request(app.getHttpServer())
      .get('/non-existent-path')
      .expect(404); // HTTP 상태 코드 404 (Not Found) 예상
  });
});

주요 코드 설명

  • import * as request from 'supertest';: HTTP 요청을 보내고 응답을 검증하는 데 사용되는 Supertest 라이브러리를 임포트합니다.
  • Test.createTestingModule({ imports: [AppModule] }).compile();: NestJS 애플리케이션을 테스트용으로 설정합니다. AppModule을 임포트하여 실제 애플리케이션의 모든 모듈, 컨트롤러, 서비스가 로드되도록 합니다. 단위 테스트와 달리 의존성을 모킹하지 않고 실제 인스턴스를 사용합니다.
  • app = moduleFixture.createNestApplication(); await app.init();: 테스트용 NestJS 애플리케이션 인스턴스를 생성하고 초기화합니다. app.init()이 호출되면 모든 DI 컨테이너가 준비되고, 라우트 핸들러가 등록되는 등 실제 서버가 구동되기 직전의 상태가 됩니다.
  • await app.close();: afterAll 훅에서 테스트가 끝난 후 애플리케이션 인스턴스를 종료하여 사용된 리소스(예: 데이터베이스 연결)를 정리합니다.
  • request(app.getHttpServer()): Supertest의 핵심 부분입니다. app.getHttpServer()를 통해 NestJS가 사용하는 Node.js HTTP 서버 인스턴스를 가져와 Supertest에 전달하면, Supertest가 해당 서버에 직접 요청을 보냅니다.
  • .get('/app/hello'), .post('/app/sum'): HTTP 메서드와 경로를 지정합니다.
  • .send({ a: 5, b: 7 }): POST 요청의 경우, 요청 본문 데이터를 전송합니다.
  • .expect(200), .expect(201), .expect(404): 예상되는 HTTP 상태 코드를 검증합니다.
  • .expect('Hello World!'), .expect('12'): 예상되는 응답 본문을 검증합니다. Supertest는 JSON 응답에 대한 검증도 강력하게 지원합니다 (예: .expect({ id: 1, name: 'Test User' })).

테스트 실행

E2E 테스트는 별도의 Jest 설정 파일을 사용하므로, package.json에 정의된 스크립트를 사용합니다.

npm run test:e2e

콘솔에 테스트 통과 결과가 나타날 것입니다.


데이터베이스 연동 E2E 테스트 시 고려사항

실제 애플리케이션의 E2E 테스트는 데이터베이스를 포함하는 경우가 많습니다. 이때는 몇 가지 중요한 고려사항이 있습니다.

  • 독립적인 테스트 데이터: 각 테스트가 독립적으로 실행될 수 있도록 테스트 데이터를 준비하고, 테스트 완료 후에는 데이터를 정리해야 합니다. 이를 위해 테스트 전용 데이터베이스를 사용하거나, 각 테스트 beforeEach/afterEach 훅에서 데이터를 초기화/클린업하는 전략을 사용합니다.
    • 테스트 컨테이너(Testcontainers): Docker 컨테이너를 사용하여 테스트 시작 시 임시 데이터베이스를 띄우고, 테스트 종료 시 제거하는 방식이 가장 강력하고 격리된 환경을 제공합니다.
  • 환경 변수 관리: 테스트 환경에서 데이터베이스 연결 정보, 외부 서비스 API 키 등 환경 변수를 다르게 설정해야 합니다. config 모듈을 활용하거나, dotenv를 사용하여 test.env 파일 등을 활용할 수 있습니다.
  • 목 서비스 vs 실제 서비스: E2E 테스트는 가능한 한 실제 서비스를 사용해야 하지만, 제어하기 어렵거나 비용이 많이 드는 외부 서비스(결제 게이트웨이, SMS 발송 서비스 등)는 여전히 모킹/스텁 처리할 수 있습니다. NestJS의 Test.createTestingModule에서 특정 프로바이더만 오버라이드(override) 하여 모킹된 버전을 주입할 수 있습니다.

예시: 특정 서비스 오버라이드

test/some.e2e-spec.ts (부분 발췌)
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { EmailService } from './../src/email/email.service'; // 이메일 서비스 (외부 의존성)

// 이메일 서비스를 모킹하여 실제 이메일 전송을 방지
const mockEmailService = {
  sendEmail: jest.fn(() => Promise.resolve('Email sent successfully')),
};

describe('User Registration (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    })
      .overrideProvider(EmailService) // EmailService를 오버라이드
      .useValue(mockEmailService) // 모킹된 EmailService 사용
      .compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/register (POST) should register user and send email', () => {
    return request(app.getHttpServer())
      .post('/register')
      .send({ username: 'testuser', email: 'test@example.com', password: 'password123' })
      .expect(201)
      .expect(res => {
        expect(res.body.message).toBe('User registered successfully');
      })
      .then(() => {
        // 모킹된 이메일 서비스의 sendEmail 메서드가 호출되었는지 검증
        expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(1);
        expect(mockEmailService.sendEmail).toHaveBeenCalledWith('test@example.com', 'Welcome testuser!');
      });
  });
});

overrideProvider()를 사용하면 전체 AppModule을 로드하면서도 특정 프로바이더만 가짜 구현으로 대체할 수 있습니다. 이는 실제 데이터베이스 연결은 사용하되, 외부 API 호출과 같은 특정 로직만 모킹하고 싶을 때 유용합니다.


E2E 테스트는 애플리케이션의 안정성과 신뢰성을 최종적으로 검증하는 데 필수적인 도구입니다. 실행 속도가 느리고 유지보수 비용이 높을 수 있지만, 사용자에게 제공될 실제 경험을 보장한다는 점에서 그 가치는 매우 큽니다. Jest와 Supertest를 활용하는 NestJS의 E2E 테스트는 직관적이고 강력한 기능을 제공하여 효과적인 테스트 전략을 구축하는 데 기여합니다.

이것으로 8장 "테스팅 전략"의 두 번째 절을 마칩니다. 다음 절에서는 테스트 커버리지 도구를 사용하여 테스트 코드의 품질을 측정하고 개선하는 방법에 대해 알아보겠습니다.