본문 바로가기

단위 테스트 작성과 모킹

테스트 모듈로 서비스와 컨트롤러를 격리하고 외부 의존성을 mock과 stub으로 대체해 단위 동작을 검증합니다.

8장에서는 테스팅 전략을 다루며, 먼저 단위 테스트(Unit Test) 작성과 모킹(Mocking) 기법을 설명합니다.

소프트웨어 개발에서 테스트는 코드의 품질을 보장하고, 예상치 못한 버그를 발견하며, 향후 코드 변경 시 회귀(regression)를 방지하는 데 필수적입니다.

NestJS의 의존성 주입 구조는 실제로 실행할 대상과 대체할 협력자를 명시하기 쉽게 해 줍니다. 다만 테스트 가능성은 프레임워크만으로 보장되지 않으며, 코드의 책임과 I/O 경계를 작게 나누는 설계가 함께 필요합니다.


테스팅의 중요성 및 종류

다음 세 범위는 자주 쓰는 구분입니다. 이름만으로 테스트를 고정하기보다 실제로 부팅하는 범위, 사용하는 의존성, 진입점을 기준으로 구분해야 합니다.

단위 테스트(Unit Test)
  • 목표: 애플리케이션의 가장 작은 독립적인 코드 조각(함수, 메서드, 클래스)이 예상대로 작동하는지 검증합니다.
  • 특징: 격리된 환경에서 수행되며, 외부 의존성(데이터베이스, 네트워크 요청 등)은 모킹하거나 스텁(Stubbing) 처리합니다.
  • 장점: 실행 속도가 빠르고, 문제 발생 시 정확한 위치를 파악하기 용이하며, 개발 단계에서 피드백을 빠르게 얻을 수 있습니다.
통합 테스트(Integration Test)
  • 목표: 여러 단위(모듈, 서비스)들이 함께 작동하여 예상대로 통신하고 동작하는지 검증합니다.
  • 특징: 실제 의존성(데이터베이스, 다른 서비스)의 일부 또는 전체를 포함하여 테스트합니다.
  • 장점: 실제 환경에 가까운 테스트를 통해 단위 테스트에서 발견하기 어려운 문제를 찾아낼 수 있습니다.
E2E 테스트(End-to-End Test)
  • 목표: 실제 애플리케이션 진입점부터 응답까지의 계약이 정상적으로 동작하는지 검증합니다.
  • 특징: Nest 애플리케이션을 초기화하고 Supertest 등으로 HTTP API를 호출합니다. 브라우저 UI 조작은 프런트엔드나 더 넓은 시스템 테스트의 범위입니다.
  • 장점: 라우팅, 프레임워크 파이프라인, provider 조립과 응답 계약을 함께 검증합니다.
  • 단점: 실행 속도가 느리고, 문제 발생 시 원인을 파악하기 어렵습니다.

아래 다이어그램은 세 범위와 서비스·컨트롤러 단위 테스트에서 실제 대상과 test double이 갈리는 지점을 함께 보여 줍니다.

NestJS의 단위·통합·E2E 테스트 범위와 서비스·컨트롤러 단위 테스트에서 실제 대상, test double, 결과와 호출 단언을 나누는 기준

Nest · Test scope

단위 테스트의 실행 경계

같은 메서드 호출도 실제로 통과한 경계에 따라 의미가 다릅니다. 서비스와 컨트롤러의 의존성을 대체하면 로직과 호출 계약을 빠르게 검증할 수 있습니다.

실제 대상과 double을 두 경계에서 구분한다 두 단위 테스트는 독립된 경계이다. 서비스 사례는 테스트가 실제 UsersService를 호출하고 DatabaseService double의 결과 또는 오류를 돌려받아 결과와 DB 호출 인자를 단언한다. 컨트롤러 사례는 테스트가 실제 AppController를 직접 호출하고 AppService double에 위임한 값과 반환 결과를 단언한다. 점선은 DI 토큰에 double을 주입하는 설정이며 HTTP 요청이나 실제 DB 연결이 아니다. 두 영역 사이에는 실행 화살표가 없다. 서비스 단위 컨트롤러 단위 호출 협력 DI 관찰 호출 협력 DI 관찰 서비스 테스트 결과와 인자 단언 실제 UsersService 직접 메서드 호출 DB double 성공 · 없음 · 오류 컨트롤러 테스트 위임과 반환 단언 실제 AppController 직접 메서드 호출 Service double 협력 결과 고정
단위 테스트 두 경계와 double 주입 두 단위 테스트는 독립된 경계이다. 서비스 사례는 테스트가 실제 UsersService를 호출하고 DatabaseService double의 결과 또는 오류를 돌려받아 결과와 DB 호출 인자를 단언한다. 컨트롤러 사례는 테스트가 실제 AppController를 직접 호출하고 AppService double에 위임한 값과 반환 결과를 단언한다. 점선은 DI 토큰에 double을 주입하는 설정이며 HTTP 요청이나 실제 DB 연결이 아니다. 두 영역 사이에는 실행 화살표가 없다. 서비스 단위 컨트롤러 단위 서비스 테스트 결과와 인자 단언 실제 UsersService 메서드 직접 호출 DB double 성공 · 없음 · 오류 컨트롤러 테스트 위임과 반환 단언 실제 AppController 메서드 직접 호출 Service double 계산을 복제하지 않음
단위

클래스 하나와 직접 맞닿은 계약

대상 메서드를 직접 호출하고 DB·네트워크 같은 경계는 double로 대체합니다.

통합

여러 provider의 조립과 협력

모듈 wiring, 실제 구현 사이의 계약, 선택한 어댑터와의 상호작용을 함께 확인합니다.

E2E

부팅한 Nest 앱의 HTTP 계약

실제 앱을 초기화하고 HTTP 요청부터 라우팅, 프레임워크 처리와 응답까지 검증합니다.

실제 대상 · Service

UsersService는 DB 호출을 위임

  • 경계 double: DatabaseService의 성공값과 빈 결과를 테스트가 지정
  • 결과 관찰: 예제는 반환값과 빈 결과를 단언. 오류는 별도 사례로 추가
  • 상호작용 관찰: 규칙상 필요한 DB 호출과 인자만 단언

실제 DB 구현과 네트워크는 이 단위 테스트의 실행 경계 밖입니다.

실제 대상 · Controller

AppController는 위임 계약을 실행

  • 경계 double: AppService가 정해진 결과나 오류를 반환
  • 위임 관찰: 요청 값에서 만든 서비스 호출 인자를 단언
  • 반환 관찰: 서비스 결과를 약속한 형태로 돌려주는지 단언

메서드 직접 호출만으로 라우팅 데코레이터, Guard, Pipe, Interceptor, Exception Filter, HTTP 직렬화가 실행되지는 않습니다.

결과 제어

Stub 역할

mockResolvedValueOnce와 mockRejectedValueOnce로 협력자의 이번 응답을 고정해 대상의 분기를 재현합니다.

호출 관찰

Mock 역할

toHaveBeenCalledWith처럼 계약상 중요한 호출 횟수·인자를 관찰합니다. 내부 구현 순서를 그대로 복제하지 않습니다.

Nest API의 E2E는 보통 초기화한 애플리케이션에 HTTP 요청을 보내는 테스트입니다. 브라우저 UI 조작은 별도의 프런트엔드·시스템 테스트 범위입니다.

이 절에서는 가장 기본이 되는 단위 테스트에 집중하여 NestJS에서 단위 테스트를 효과적으로 작성하는 방법을 알아봅니다.


NestJS에서 단위 테스트 설정

이 장은 Jest를 설정한 프로젝트의 테스트 예제입니다. 최신 Nest 공식 테스팅 문서는 새 프로젝트의 기본 러너를 Vitest로 안내합니다. Nest의 테스트 모듈과 DI 기능은 러너에 종속되지 않지만, 아래 jest.fn()과 설정 파일을 쓰려면 Jest 의존성·TypeScript 변환·스크립트를 프로젝트에 구성해야 합니다. 생성 파일과 기본 러너는 CLI 버전과 프로젝트 옵션에 따라 달라질 수 있습니다.

이 교재 앞부분의 CLI 11 재현 경로를 이어왔다면 Nest schematics 11.0.9의 TypeScript 스타터에 Jest와 test, test:e2e, test:cov 스크립트가 있습니다. 최신 CLI로 새로 만든 프로젝트와 이 기준선을 섞지 말고, 먼저 자신의 package.json에서 실제 러너와 스크립트를 확인하세요. 아래 예제는 Jest를 선택한 상태에서 진행합니다.

기본 파일 구조
app.controller.ts
app.controller.spec.ts
app.module.ts
app.service.ts
app.e2e-spec.ts # E2E 테스트 예시
package.json # test 스크립트와 Jest 설정

Jest를 실행하도록 test 스크립트를 구성한 프로젝트에서는 npm test로 시작할 수 있습니다. TypeScript 변환, 테스트 환경, 경로 별칭 설정도 프로젝트 구성에 맞게 확인해야 합니다.

이제 특정 서비스에 대한 단위 테스트를 작성해 보겠습니다.


단위 테스트 작성: AppService 예시

가장 간단한 AppService의 단위 테스트를 예로 들어보겠습니다.

기존 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;
  }
}
AppService에 대한 단위 테스트 코드

NestJS는 @nestjs/testing 패키지를 통해 테스트 유틸리티를 제공합니다. 다음 예시는 소스 옆에 src/app.service.spec.ts 파일을 직접 만든 경우입니다.

src/app.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AppService } from './app.service';

describe('AppService', () => { // 'describe' 블록은 테스트 그룹을 정의합니다.
  let service: AppService; // 테스트할 서비스 인스턴스

  // 각 테스트 실행 전에 이 블록이 실행됩니다.
  // 여기서는 NestJS 테스트 모듈을 설정하고 서비스 인스턴스를 가져옵니다.
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [AppService], // 테스트할 서비스(AppService)를 프로바이더로 등록
    }).compile(); // 모듈 컴파일

    service = module.get<AppService>(AppService); // 컴파일된 모듈에서 AppService 인스턴스 가져오기
  });

  // 'it' 또는 'test' 블록은 개별 테스트 케이스를 정의합니다.
  it('should be defined', () => {
    // AppService 인스턴스가 성공적으로 생성되었는지 확인
    expect(service).toBeDefined();
  });

  it('should return "Hello World!"', () => {
    // getHello() 메서드가 예상된 문자열을 반환하는지 확인
    expect(service.getHello()).toBe('Hello World!');
  });

  it('should return the sum of two numbers', () => {
    // sum() 메서드가 올바른 합계를 반환하는지 확인
    expect(service.sum(1, 2)).toBe(3);
    expect(service.sum(-1, 1)).toBe(0);
    expect(service.sum(0, 0)).toBe(0);
  });
});
테스트 실행
npm run test # 또는 npm test

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


모킹과 스텁

단위 테스트 작성과 모킹에서는 실제 DB나 외부 API를 실행하지 않고, 테스트 대상이 의존성에 어떤 값을 요청하고 어떤 결과를 돌려받는지만 고정합니다.

실제 애플리케이션에서는 서비스가 다른 서비스나 데이터베이스, 외부 API 등 다양한 의존성을 가집니다.

단위 테스트는 이런 의존성을 배제하고 테스트 대상 코드만 격리하여 테스트해야 합니다.

이때 모킹(Mocking)과 스텁(Stubbing)이 사용됩니다.

  • 스텁 역할(Stub role): test double의 반환값이나 오류를 미리 정해 대상이 실행할 분기를 통제합니다.
  • 목 역할(Mock role): test double이 호출됐는지, 몇 번·어떤 인자로 호출됐는지처럼 의미 있는 상호작용을 관찰합니다. 하나의 jest.fn()이 한 사례에서 두 역할을 함께 맡을 수도 있습니다.

NestJS에서는 Jest의 모킹 기능을 활용하여 의존성을 쉽게 모킹할 수 있습니다.

시나리오: UsersService가 DatabaseService에 의존한다고 가정하고, UsersService의 단위 테스트를 작성할 때 DatabaseService를 모킹해 봅시다.

UsersService 코드
src/users/users.service.ts
import { Injectable } from '@nestjs/common';

interface User {
  id: number;
  name: string;
  email: string;
}

// 가상의 DatabaseService (실제로는 ORM이나 DB 클라이언트가 될 수 있음)
@Injectable()
export class DatabaseService {
  private users: User[] = [
    { id: 1, name: 'Alice', email: '[email protected]' },
    { id: 2, name: 'Bob', email: '[email protected]' },
  ];
  private nextId = 3;

  async findOneUser(id: number): Promise<User | undefined> {
    // 실제 DB 호출 로직 (비동기)
    console.log(`[DB] Finding user with ID: ${id}`);
    return Promise.resolve(this.users.find(user => user.id === id));
  }

  async createUser(user: { name: string; email: string }): Promise<User> {
    // 실제 DB 삽입 로직 (비동기)
    const newUser = { id: this.nextId++, ...user };
    this.users.push(newUser);
    console.log(`[DB] Creating user: ${newUser.name}`);
    return Promise.resolve(newUser);
  }
}

@Injectable()
export class UsersService {
  constructor(private readonly databaseService: DatabaseService) {} // DatabaseService 의존성 주입

  async getUserById(id: number): Promise<User | undefined> {
    return this.databaseService.findOneUser(id);
  }

  async createUser(name: string, email: string): Promise<User> {
    // 사용자 생성 전 추가 로직이 있을 수 있음
    const newUser = await this.databaseService.createUser({ name, email });
    // 예를 들어, 환영 이메일 발송 등
    return newUser;
  }
}

위 DatabaseService는 비동기 메서드를 가진 메모리 저장소 예제입니다. 주석에 DB 호출이라고 적혀 있어도 실제 DB 연결은 없습니다. 이어지는 테스트는 이 클래스의 저장 동작까지 실행하는 대신, 같은 DI 토큰에 주입한 double과 UsersService의 협력만 검사합니다.

UsersService에 대한 단위 테스트 (모킹 활용)
src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { DatabaseService } from './users.service';

const createDatabaseServiceMock = () => ({
  findOneUser: jest.fn(),
  createUser: jest.fn(),
});

describe('UsersService', () => {
  let service: UsersService;
  let databaseServiceMock: ReturnType<typeof createDatabaseServiceMock>;

  beforeEach(async () => {
    // 테스트마다 새 double을 만들어 호출 기록과 구현을 공유하지 않습니다.
    databaseServiceMock = createDatabaseServiceMock();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: DatabaseService,
          useValue: databaseServiceMock,
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getUserById', () => {
    it('should return a user if found', async () => {
      databaseServiceMock.findOneUser.mockResolvedValueOnce({
        id: 1,
        name: 'MockUser1',
        email: '[email protected]',
      });

      const user = await service.getUserById(1);

      expect(user).toEqual({ id: 1, name: 'MockUser1', email: '[email protected]' });
      expect(databaseServiceMock.findOneUser).toHaveBeenCalledTimes(1);
      expect(databaseServiceMock.findOneUser).toHaveBeenCalledWith(1);
    });

    it('should return undefined if user not found', async () => {
      databaseServiceMock.findOneUser.mockResolvedValueOnce(undefined);

      const user = await service.getUserById(2);

      expect(user).toBeUndefined();
      expect(databaseServiceMock.findOneUser).toHaveBeenCalledWith(2);
    });
  });

  describe('createUser', () => {
    it('should create and return a new user', async () => {
      databaseServiceMock.createUser.mockResolvedValueOnce({
        id: 99,
        name: 'New User',
        email: '[email protected]',
      });

      const newUser = await service.createUser('New User', '[email protected]');

      expect(newUser).toEqual({ id: 99, name: 'New User', email: '[email protected]' });
      expect(databaseServiceMock.createUser).toHaveBeenCalledTimes(1);
      expect(databaseServiceMock.createUser).toHaveBeenCalledWith({ name: 'New User', email: '[email protected]' });
    });
  });
});
주요 모킹 기법 설명
  • jest.fn(): Jest의 mock 함수를 생성합니다. 각 사례에서 반환값이나 reject를 정하고 호출 횟수와 인자를 관찰할 수 있습니다.
  • provide: DatabaseService, useValue: databaseServiceMock: Test.createTestingModule의 providers 배열에서 DatabaseService 토큰에 실제 클래스 대신 double을 주입하도록 설정합니다. 이를 통해 UsersService는 실제 데이터베이스 대신 모킹된 객체와 상호작용합니다.
  • expect(databaseServiceMock.findOneUser).toHaveBeenCalledWith(1);: findOneUser가 계약상 필요한 인자로 호출되었는지 검증합니다.
  • 예시처럼 beforeEach에서 새 double을 만들면 테스트 사이에 호출 기록과 임시 구현이 공유되지 않습니다. 공유 mock을 선택했다면 clear, reset, restore의 차이를 알고 필요한 범위에서만 정리해야 합니다.

예시 외부 의존성이 실패할 때도 결과를 확인하기

사용자를 찾지 못해 undefined를 돌려주는 경우와 조회 자체가 실패한 경우는 다릅니다. 앞의 테스트 파일에서 service와 databaseServiceMock을 준비하는 바깥 describe 안에 다음 사례를 추가합니다. 서비스는 오류를 잡거나 HTTP 응답으로 바꾸지 않으므로, double이 거절한 Promise의 오류가 호출자에게 그대로 전달되어야 합니다.

src/users/users.service.spec.ts (추가할 실패 사례)
it('should propagate a database failure', async () => {
  const failure = new Error('DB offline');
  databaseServiceMock.findOneUser.mockRejectedValueOnce(failure);

  await expect(service.getUserById(7)).rejects.toBe(failure);
  expect(databaseServiceMock.findOneUser).toHaveBeenCalledTimes(1);
  expect(databaseServiceMock.findOneUser).toHaveBeenCalledWith(7);
});

실행 순서는 beforeEach의 새 double 생성 → 한 번 거절할 결과 설정 → 실제 서비스 호출 → 오류와 협력 인자 단언입니다. await를 빼면 비동기 단언이 끝나기 전에 테스트가 종료될 수 있습니다. 이 검사는 DB 장애 시 HTTP가 500인지 확인하는 테스트는 아닙니다. HTTP 상태는 컨트롤러 이후 예외 처리 경계에서 별도로 확인합니다.

직접 판단하기 다음 테스트의 beforeEach가 실행된 직후에는 이전 사례의 DB offline 설정이나 호출 기록이 남아 있을까요? 이 새 double에 아무 반환값도 설정하지 않고 getUserById(7)을 호출하면 결과는 무엇일까요?

답과 이유 확인

매번 createDatabaseServiceMock()을 호출하므로 이전 double의 설정과 기록은 공유되지 않습니다. 새 jest.fn()은 구현을 지정하지 않았을 때 undefined를 반환하고, async getUserById()는 그 값을 이행한 Promise로 감쌉니다. 따라서 이 조건에서는 undefined를 받으며 DB 장애를 재현하지 않습니다. 실패 조건을 검사하려면 이번 사례에서도 reject를 직접 설정해야 합니다.


NestJS 컨트롤러 단위 테스트 (간단한 예시)

컨트롤러는 주로 서비스에 의존하므로, 컨트롤러를 단위 테스트할 때는 서비스도 모킹하는 것이 일반적입니다.

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);
  }
}
src/app.controller.spec.ts (새로 생성)
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

const createAppServiceMock = () => ({
  getHello: jest.fn(),
  sum: jest.fn(),
});

describe('AppController', () => {
  let appController: AppController;
  let appServiceMock: ReturnType<typeof createAppServiceMock>;

  beforeEach(async () => {
    appServiceMock = createAppServiceMock();

    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [
        {
          provide: AppService,
          useValue: appServiceMock,
        },
      ],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  it('should be defined', () => {
    expect(appController).toBeDefined();
  });

  describe('getHello', () => {
    it('should return "Hello from Mock!"', () => {
      appServiceMock.getHello.mockReturnValueOnce('Hello from Mock!');

      const result = appController.getHello();

      expect(result).toBe('Hello from Mock!');
      expect(appServiceMock.getHello).toHaveBeenCalledTimes(1);
    });
  });

  describe('sumNumbers', () => {
    it('should return the sum from the service', () => {
      // 계산을 mock에 복제하지 않고 이번 협력 결과만 정합니다.
      appServiceMock.sum.mockReturnValueOnce(8);

      const result = appController.sumNumbers({ a: 5, b: 3 });

      expect(result).toBe(8);
      expect(appServiceMock.sum).toHaveBeenCalledTimes(1);
      expect(appServiceMock.sum).toHaveBeenCalledWith(5, 3);
    });
  });
});

이 테스트는 컨트롤러 메서드를 직접 호출하므로 서비스 위임과 반환값은 검증하지만, 라우팅 데코레이터와 Guard, Pipe, Interceptor, Exception Filter, HTTP 직렬화 같은 프레임워크 요청 파이프라인은 실행하지 않습니다. 그 경로가 목적이라면 초기화한 애플리케이션에 HTTP 요청을 보내는 통합 또는 E2E 테스트를 별도로 작성합니다.

단위 테스트가 안정적으로 유지되려면 double의 계약과 수명, 결과·오류·상호작용의 검증 지점을 함께 관리해야 합니다.

NestJS 모킹 테스트의 계약 설계와 격리 정책

새 double·DI 조립·성공/거절 지정·대상 실행·단언·다음 새 double로 테스트 기록의 수명이 바뀐다.

NestJS 모킹 테스트에서 책임 경계를 고르고 실제 계약을 닮은 double을 조립한 뒤 결과·오류·의미 있는 호출을 단언하고 Jest mock 상태를 격리하는 순서

Nest · Mock lifecycle

Mock 구성과 정리의 순서

테스트할 계약을 정한 뒤 의존성을 대체하고 결과와 호출을 단언합니다. 호출 이력, 구현, 원래 속성의 복원은 서로 다른 정리 작업입니다.

실행·단언을 마친 다음 사례는 새 상태로 돌아간다 한 사례에서 경계를 정하고 새 double을 만든 뒤 TestingModule에서 DI 토큰으로 조립한다. 이번 성공값 또는 거절을 지정하고 실제 대상을 실행해 결과와 의미 있는 호출만 단언한다. 다음 beforeEach는 다시 새 double로 돌아가므로 이전 기록과 구현을 공유하지 않는다. 마지막 점선은 다음 사례 준비이고 clear/reset/restore를 모두 연속 수행하라는 뜻이 아니다. 공유 mock이나 spy를 선택한 경우의 정리 차이는 아래 비교를 따른다. 다음 사례 새 객체 경계 선택 대상 · 협력자 새 double 생성 beforeEach DI 토큰 조립 useValue 이번 응답 지정 성공 · 없음 · reject 실제 대상 호출 동기 / Promise 계약 결과와 호출 단언 await · 값 · 인자 다음 beforeEach 이전 상태 공유 없음
사례마다 새 double로 시작하는 수명 한 사례에서 경계를 정하고 새 double을 만든 뒤 TestingModule에서 DI 토큰으로 조립한다. 이번 성공값 또는 거절을 지정하고 실제 대상을 실행해 결과와 의미 있는 호출만 단언한다. 다음 beforeEach는 다시 새 double로 돌아가므로 이전 기록과 구현을 공유하지 않는다. 마지막 점선은 다음 사례 준비이고 clear/reset/restore를 모두 연속 수행하라는 뜻이 아니다. 공유 mock이나 spy를 선택한 경우의 정리 차이는 아래 비교를 따른다. 경계 선택 실제 대상 · 협력자 새 double 생성 beforeEach TestingModule 조립 토큰 → useValue 이번 응답 지정 성공 · 없음 · reject 실제 대상 호출 동기 / Promise 계약 결과와 호출 단언 await · 값 · 인자 다음 beforeEach 이전 상태 공유 없음
  1. 대상 책임과 실행 경계를 고른다

    서비스 규칙을 볼지, 컨트롤러 위임을 볼지 먼저 정합니다. DB·네트워크·다른 provider처럼 경계 밖 협력자만 대체합니다.

  2. 실제 의존성 계약을 닮은 double을 만든다

    주입 토큰, 메서드 이름, 인자, 동기·Promise 반환 형태를 맞춥니다. double 안에 대상과 같은 계산·분기 로직을 복사하지 않습니다.

  3. TestingModule에 실제 대상과 double을 조립한다

    provide는 실제 토큰을 가리키고 useValue는 이번 테스트가 통제하는 객체를 제공합니다.

  4. 이번 사례의 응답을 지정하고 대상만 실행한다

    mockResolvedValueOnce, mockRejectedValueOnce처럼 사례별 성공·빈 결과·실패를 준비한 뒤 서비스나 컨트롤러 메서드를 호출합니다.

  5. 결과·오류와 의미 있는 상호작용을 단언한다

    외부에 보이는 반환값과 예외를 우선 확인하고, 계약상 필요한 호출 횟수·인자만 추가합니다. 우연한 내부 호출 순서는 피합니다.

  6. 다음 사례가 새 상태에서 시작하도록 격리한다

    기본 정책은 beforeEach에서 새 double과 모듈을 만드는 것입니다. 공유 mock이나 spy를 썼을 때만 해당 수명에 맞는 정리 동작을 고릅니다.

clear

jest.clearAllMocks()

모든 mock의 호출·인스턴스·context·result 기록을 비우지만, 지정한 구현과 반환값은 유지합니다.

reset

jest.resetAllMocks()

기록을 비우고 mock 구현을 undefined를 반환하는 빈 함수로 바꿉니다. 원래 함수 구현을 복구하는 동작은 아닙니다.

restore

jest.restoreAllMocks()

jest.spyOn과 jest.replaceProperty로 바꾼 대상을 원래 구현·값으로 되돌립니다. 일반 jest.fn은 자동 복구되지 않습니다.

fresh first

새 double이 가장 단순한 기본값

가변 반환값이나 호출 기록을 공유하지 않으므로 테스트 순서에 덜 민감합니다. beforeEach에서 생성하면 수명이 코드에 드러납니다.

hook by owner

정리 hook은 만든 범위가 책임진다

모든 테스트에 전역 clear를 관성적으로 두지 않습니다. 공유 spy는 afterEach에서 restore하고, 공유 mock은 의도에 따라 clear 또는 reset합니다.

초기화 API는 서로 바꿔 쓸 수 없습니다. “기록만 비울지, 가짜 구현도 지울지, 실제 구현으로 되돌릴지”를 테스트 격리 요구에 맞춰 선택합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 한 사례에서 경계를 정하고 새 double을 만든 뒤 TestingModule에서 DI 토큰으로 조립한다. 이번 성공값 또는 거절을 지정하고 실제 대상을 실행해 결과와 의미 있는 호출만 단언한다. 다음 beforeEach는 다시 새 double로 돌아가므로 이전 기록과 구현을 공유하지 않는다. 마지막 점선은 다음 사례 준비이고 clear/reset/restore를 모두 연속 수행하라는 뜻이 아니다. 공유 mock이나 spy를 선택한 경우의 정리 차이는 아래 비교를 따른다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1NestJS 모킹 테스트의 계약 설계와 격리 정책
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
새 double·DI

사례마다 새 double을 만들어 TestingModule에 조립합니다.

double: 새 객체 · 연결: DI token이전 기록이 없는 시험 입력이 생깁니다.
행동과 대상 실행

이번 성공·거절을 지정하고 실제 대상을 호출합니다.

입력: 이번 행동 · 실행: 실제 대상대역 계약으로 실제 결과를 관찰합니다.
단언·다음 새 사례

결과·의미 있는 호출을 단언하고 다음 beforeEach에서는 새 double을 만듭니다.

단언: 결과·의미 있는 호출 · 다음: 새 double사례 사이 기록·구현을 공유하지 않습니다.

NestJS 모킹 테스트의 계약 설계와 격리 정책

새 double·DI 조립·성공/거절 지정·대상 실행·단언·다음 새 double로 테스트 기록의 수명이 바뀐다.

NestJS 모킹 테스트에서 책임 경계를 고르고 실제 계약을 닮은 double을 조립한 뒤 결과·오류·의미 있는 호출을 단언하고 Jest mock 상태를 격리하는 순서

Nest · Mock lifecycle

Mock 구성과 정리의 순서

테스트할 계약을 정한 뒤 의존성을 대체하고 결과와 호출을 단언합니다. 호출 이력, 구현, 원래 속성의 복원은 서로 다른 정리 작업입니다.

실행·단언을 마친 다음 사례는 새 상태로 돌아간다 한 사례에서 경계를 정하고 새 double을 만든 뒤 TestingModule에서 DI 토큰으로 조립한다. 이번 성공값 또는 거절을 지정하고 실제 대상을 실행해 결과와 의미 있는 호출만 단언한다. 다음 beforeEach는 다시 새 double로 돌아가므로 이전 기록과 구현을 공유하지 않는다. 마지막 점선은 다음 사례 준비이고 clear/reset/restore를 모두 연속 수행하라는 뜻이 아니다. 공유 mock이나 spy를 선택한 경우의 정리 차이는 아래 비교를 따른다. 다음 사례 새 객체 경계 선택 대상 · 협력자 새 double 생성 beforeEach DI 토큰 조립 useValue 이번 응답 지정 성공 · 없음 · reject 실제 대상 호출 동기 / Promise 계약 결과와 호출 단언 await · 값 · 인자 다음 beforeEach 이전 상태 공유 없음
사례마다 새 double로 시작하는 수명 한 사례에서 경계를 정하고 새 double을 만든 뒤 TestingModule에서 DI 토큰으로 조립한다. 이번 성공값 또는 거절을 지정하고 실제 대상을 실행해 결과와 의미 있는 호출만 단언한다. 다음 beforeEach는 다시 새 double로 돌아가므로 이전 기록과 구현을 공유하지 않는다. 마지막 점선은 다음 사례 준비이고 clear/reset/restore를 모두 연속 수행하라는 뜻이 아니다. 공유 mock이나 spy를 선택한 경우의 정리 차이는 아래 비교를 따른다. 경계 선택 실제 대상 · 협력자 새 double 생성 beforeEach TestingModule 조립 토큰 → useValue 이번 응답 지정 성공 · 없음 · reject 실제 대상 호출 동기 / Promise 계약 결과와 호출 단언 await · 값 · 인자 다음 beforeEach 이전 상태 공유 없음
  1. 대상 책임과 실행 경계를 고른다

    서비스 규칙을 볼지, 컨트롤러 위임을 볼지 먼저 정합니다. DB·네트워크·다른 provider처럼 경계 밖 협력자만 대체합니다.

  2. 실제 의존성 계약을 닮은 double을 만든다

    주입 토큰, 메서드 이름, 인자, 동기·Promise 반환 형태를 맞춥니다. double 안에 대상과 같은 계산·분기 로직을 복사하지 않습니다.

  3. TestingModule에 실제 대상과 double을 조립한다

    provide는 실제 토큰을 가리키고 useValue는 이번 테스트가 통제하는 객체를 제공합니다.

  4. 이번 사례의 응답을 지정하고 대상만 실행한다

    mockResolvedValueOnce, mockRejectedValueOnce처럼 사례별 성공·빈 결과·실패를 준비한 뒤 서비스나 컨트롤러 메서드를 호출합니다.

  5. 결과·오류와 의미 있는 상호작용을 단언한다

    외부에 보이는 반환값과 예외를 우선 확인하고, 계약상 필요한 호출 횟수·인자만 추가합니다. 우연한 내부 호출 순서는 피합니다.

  6. 다음 사례가 새 상태에서 시작하도록 격리한다

    기본 정책은 beforeEach에서 새 double과 모듈을 만드는 것입니다. 공유 mock이나 spy를 썼을 때만 해당 수명에 맞는 정리 동작을 고릅니다.

clear

jest.clearAllMocks()

모든 mock의 호출·인스턴스·context·result 기록을 비우지만, 지정한 구현과 반환값은 유지합니다.

reset

jest.resetAllMocks()

기록을 비우고 mock 구현을 undefined를 반환하는 빈 함수로 바꿉니다. 원래 함수 구현을 복구하는 동작은 아닙니다.

restore

jest.restoreAllMocks()

jest.spyOn과 jest.replaceProperty로 바꾼 대상을 원래 구현·값으로 되돌립니다. 일반 jest.fn은 자동 복구되지 않습니다.

fresh first

새 double이 가장 단순한 기본값

가변 반환값이나 호출 기록을 공유하지 않으므로 테스트 순서에 덜 민감합니다. beforeEach에서 생성하면 수명이 코드에 드러납니다.

hook by owner

정리 hook은 만든 범위가 책임진다

모든 테스트에 전역 clear를 관성적으로 두지 않습니다. 공유 spy는 afterEach에서 restore하고, 공유 mock은 의도에 따라 clear 또는 reset합니다.

초기화 API는 서로 바꿔 쓸 수 없습니다. “기록만 비울지, 가짜 구현도 지울지, 실제 구현으로 되돌릴지”를 테스트 격리 요구에 맞춰 선택합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 한 사례에서 경계를 정하고 새 double을 만든 뒤 TestingModule에서 DI 토큰으로 조립한다. 이번 성공값 또는 거절을 지정하고 실제 대상을 실행해 결과와 의미 있는 호출만 단언한다. 다음 beforeEach는 다시 새 double로 돌아가므로 이전 기록과 구현을 공유하지 않는다. 마지막 점선은 다음 사례 준비이고 clear/reset/restore를 모두 연속 수행하라는 뜻이 아니다. 공유 mock이나 spy를 선택한 경우의 정리 차이는 아래 비교를 따른다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1NestJS 모킹 테스트의 계약 설계와 격리 정책
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
새 double·DI

사례마다 새 double을 만들어 TestingModule에 조립합니다.

double: 새 객체 · 연결: DI token이전 기록이 없는 시험 입력이 생깁니다.
행동과 대상 실행

이번 성공·거절을 지정하고 실제 대상을 호출합니다.

입력: 이번 행동 · 실행: 실제 대상대역 계약으로 실제 결과를 관찰합니다.
단언·다음 새 사례

결과·의미 있는 호출을 단언하고 다음 beforeEach에서는 새 double을 만듭니다.

단언: 결과·의미 있는 호출 · 다음: 새 double사례 사이 기록·구현을 공유하지 않습니다.