본문으로 건너뛰기

안동민 개발노트

본문 시작

E2E 테스트 구현

실제 애플리케이션에 HTTP 요청을 보내는 E2E 환경을 구성하고 정상·실패 응답과 데이터 정리 경계를 검증합니다.

E2E 테스트를 구현할 때 상태 준비, 요청 시나리오, 실패 응답, 데이터 정리 기준을 묶었습니다.

E2E는 사용자 요청이 앱 전체를 통과하는지 확인한다

단위 테스트와 달리 실제 Nest 애플리케이션을 초기화하고, Supertest로 HTTP 요청을 보내 응답과 저장 결과를 함께 봅니다.

  1. 앱과 상태 준비

    Prepare AppModule을 띄우고 테스트 데이터나 환경 변수를 고정합니다.

  2. HTTP 호출

    Request 사용자가 보낼 요청과 같은 method, path, body를 보냅니다.

  3. 응답 검증

    Assert 상태 코드, body, header, 저장 결과를 함께 확인합니다.

  4. 데이터 정리

    Clean 다음 테스트가 같은 조건에서 시작하도록 흔적을 지웁니다.

이 절에서는 사용자 관점에서 애플리케이션의 전체 흐름을 검증하는 E2E(End-to-End) 테스트 구현 방법을 다룹니다.

단위 테스트가 개별 코드 조각의 정확성을 보장한다면, E2E 테스트는 여러 서비스, 데이터베이스, 네트워크 등 실제 환경에 가까운 조건에서 시스템 전체가 예상대로 작동하는지를 검증합니다.

이는 애플리케이션이 프로덕션 환경에서 사용자에게 어떤 경험을 제공할지 미리 확인하는 중요한 단계입니다.

다음 개요는 E2E 테스트를 구현할 때 준비해야 할 환경, 요청 경로, 데이터 격리 지점을 먼저 잡아 줍니다.

E2E 준비는 앱, 환경, 데이터, HTTP 경계를 먼저 고정한다

테스트가 느리고 넓은 만큼 시작 조건이 흐리면 실패 원인을 찾기 어렵습니다. 준비 요소를 네 갈래로 나눠 잡습니다.

  1. TestingModule + AppModule

    App 실제 모듈 구성을 로드하고 Nest 애플리케이션을 초기화합니다. await app.init()

  2. Supertest 서버 핸들

    HTTP 브라우저 대신 테스트 코드가 HTTP 서버에 직접 요청을 보냅니다. request(app.getHttpServer())

  3. 테스트 전용 설정

    Env DB 주소, API 키, feature flag를 운영 환경과 분리합니다.

  4. 독립 데이터

    Data 각 테스트가 필요한 데이터를 만들고 끝나면 정리합니다.


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$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "moduleNameMapper": {
    "^src/(.*)$": "<rootDir>/src/$1"
  }
}
  • testRegex: .e2e-spec.ts로 끝나는 파일만 E2E 테스트로 인식하여 실행합니다. 이는 단위 테스트와 E2E 테스트를 분리하여 실행할 때 유용합니다.
  • moduleNameMapper: src/ 경로 별칭을 테스트 환경의 프로젝트 경로로 연결하는 선택 설정입니다.

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 테스트는 데이터베이스를 포함하는 경우가 많습니다.

이때는 몇 가지 중요한 고려사항이 있습니다.

E2E 데이터는 준비와 정리가 테스트의 일부다

실제 DB를 쓰는 E2E는 데이터가 남으면 다음 테스트가 오염됩니다. 준비, 실행, 정리 책임을 명시해야 합니다.

격리 지점해야 할 일실패 신호
Test DB운영 DB와 다른 연결 정보를 사용한다.운영 데이터가 바뀌거나 테스트가 느려진다.
Seed테스트가 필요한 사용자·주문을 직접 만든다.실행 순서에 따라 결과가 달라진다.
CleanupafterEach/afterAll에서 생성 데이터를 지운다.중복 key, 남은 세션, 다음 테스트 실패
External API핵심 흐름이 아니면 test double이나 sandbox를 쓴다.외부 장애가 테스트 실패로 번진다.
  • 독립적인 테스트 데이터: 각 테스트가 독립적으로 실행될 수 있도록 테스트 데이터를 준비하고, 테스트 완료 후에는 데이터를 정리해야 합니다. 이를 위해 테스트 전용 데이터베이스를 사용하거나, 각 테스트 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 애플리케이션의 HTTP 경계를 검증하는 데 사용할 수 있습니다.

이것으로 8장 테스팅 전략의 두 번째 절을 마칩니다.

다음 절에서는 테스트 커버리지 도구를 사용하여 테스트 코드의 품질을 측정하고 개선하는 방법에 대해 알아보겠습니다.


E2E 테스트는 실제 흐름을 넓게 확인하기 때문에 격리 기준이 흐려지기 쉽습니다.

아래 다이어그램은 앱 초기화부터 데이터 정리까지의 기준선을 요약합니다.

앱 초기화부터 데이터 정리까지 한 줄 기준선을 둔다

E2E 테스트는 넓게 검증하기 때문에 기본 순서가 흔들리면 실패 원인이 흐려집니다.

  1. Compile

    AppModule을 테스트 모듈로 컴파일합니다.

  2. Init

    Nest 앱을 만들고 라우트를 준비합니다.

  3. Seed

    시나리오에 필요한 데이터를 만듭니다.

  4. Request

    Supertest로 실제 HTTP 요청을 보냅니다.

  5. Cleanup

    앱과 데이터를 닫고 흔적을 지웁니다.

이어지는 다이어그램은 E2E 테스트의 안정성을 유지하려면 실패가 난 지점을 준비, 실행, 검증, 정리 단계 중 어디인지 바로 좁혀야 한다는 점을 보여줍니다.

E2E 실패는 준비, 요청, 검증, 정리 중 어디인지 먼저 좁힌다

넓은 테스트가 실패하면 원인이 많아 보입니다. 단계별 신호를 보면 고칠 위치가 빨리 보입니다.

  1. 1
    앱이 뜨지 않음

    Prepare 모듈 import, 환경 변수, DB 연결 정보를 먼저 봅니다.

  2. 2
    상태 코드가 다름

    Request method, path, body, 인증 header가 실제 계약과 같은지 확인합니다.

  3. 3
    body가 예상과 다름

    Assert 응답 DTO, 저장 결과, 비동기 처리 완료 시점을 확인합니다.

  4. 4
    다음 테스트가 실패

    Cleanup 남은 row, 열린 connection, 호출 순서 의존성을 찾습니다.

마지막으로 E2E 테스트를 설계할 때 앱 준비, 데이터 구성, HTTP 호출, 검증과 정리 책임을 한 번에 점검합니다.

아래 다이어그램은 실제 요청 경계와 테스트 격리 기준을 묶어 보여줍니다.

좋은 E2E는 요청 경계와 데이터 격리 기준을 함께 가진다

시나리오가 사용자 관점으로 읽히면서도, 테스트 환경과 정리 책임은 코드 안에서 분명해야 합니다.

  1. 무엇을 실제로 띄우나

    App AppModule, guard, pipe, interceptor 범위를 정합니다.

  2. 무엇을 미리 만드나

    Data 필요한 row만 seed하고, 테스트 끝에 지웁니다.

  3. 무슨 요청을 보내나

    HTTP method, path, body, header가 외부 계약과 맞아야 합니다.

  4. 무엇으로 성공을 보나

    Assert 상태 코드와 응답 body, 저장 결과를 함께 단언합니다.