NestJS - 테스트 자동화


이 포스트는 테스트 자동화에 대해 알아보고, Jest 를 사용하여 단위 테스트를 진행하여 Test Coverage 측정까지 해본다.

소스는 user-service 에 있습니다.


1. Test

개발 프로세스 모델 중 하나로 V-모델 이 있다.

V-모델

업무의 진행은 좌상단 요구 사항 분석에서 시작하여 시스템 설계, 아키텍처 설계, 모듈(상세) 설계, 코딩 순으로 진행되며, 테스트는 역순으로 수행한다.

개발 단계테스트 종류내용
요구 사항 분석
(Requirements Analysis)
인수 테스트
(Acceptance Testing)
- 알파 테스트와 같이 실사용 환경에서의 문제 확인
- 시스템을 운영 환경에 배포할 준비가 되었는지 확인
시스템 설계
(System Design)
시스템 테스트
(System Testing)
- 환경의 제약 사항으로 인해 발생하는 문제를 찾기 위함

- Recovery Test: 정전 등과 같은 상황 발생 시 자동 초기화, 데이터 회복 확인
- Security Test: 해킹 공격으로부터 안전한지 확인
- Stress Test: 급격한 트래픽 증가에 대해 안전한지 확인
- Sensitivity Test: 잘못된 데이터나 경계값이 정상 동작하는지 확인
- Performance Test: 응답 시간, 응답 처리량 등 시스템 자원이 효율적으로 사용되고 있는지 확인
아키텍처 설계
(Architecture Design)
통합 테스트
(Integration Testing)
- 여러 모듈이 함께 동작했을 때 문제가 없는지 검증
- 코드 레벨에서 코드를 파악하지 않고 수행하기 때문에 블랙박스 테스트라고도 함
모듈 설계
(Module Design)
단위 테스트
(Unit Testing)
- 테스트 과정에서 가장 먼저 수행되는 테스트
- 코드가 정상 동작하는지 저수준에서 테스트 코드를 작성하여 검증
- 코드를 제대로 파악하고 있는 개발자가 테스트 코드를 작성하는 것이 대부눈(테스트 엔지니어가 작성하기도 함)
코딩
(Coding)
디버깅
(Debugging)
- 개발자가 직접 코드의 로직을 검증하고 오류를 디버깅하는 과정

테스트는 프로그램 실행 여부에 따라 정적 테스트와 동적 테스트로 나뉜다.

  • 정적 테스트
    • 코드를 수행하지 않고 검증하는 테스트
    • static analyzer 를 이용하여 코드에 내재된 이슈를 미리 파악
    • 동료 코드 리뷰를 받는 것도 정적 테스트
  • 동적 테스트
    • 프로그램을 실행해보면서 진행하는 테스트

V-모델의 테스트 단계에서 뒤쪽 단계의 테스트로 갈수록 이슈 수정 비용이 크게 증가하므로 테스트 코드를 작성하는 것이 결코 개발 속도를 더 느리게 만드는 것이 아님!

TDD (Test-Driven Development, 테스트 주도 개발) 을 활용하여 테스트 코드를 먼저 작성하고 이를 기반으로 실제 소프트웨어의 코드를 작성해나가는 방법론도 있다.

테스트 코드가 준비되었으면 배포 과정에 포함하도록 하는 것이 좋다. CI/CD 과정에 포함된 자동화 테스트는 통합/배포 과정에서 소스 코드 Repository 에 버그가 올라가는 것을 방지해준다.


2. Jest

<테스트 프레임워크의 구성 요소>

  • Test Runner
    • 테스트가 실행되는 환경 제공
  • Assertion
    • 테스트의 상황을 가정
  • Matcher
    • 테스트의 기대 결과를 비교
  • Test Double
    • 테스트 과정에서 현재 테스트 대상 모듈이 의존하는 다른 모듈을 임의의 것으로 대체
    • Dummy, Fake, Stub, Spy, Mock 등

Nest 는 기본 테스트 프레임워크로 Jest 와 SuperTest 를 제공한다.

Jest 는 메타가 주도해서 만든 프레임워크로 Nest 뿐 아니라 Babel, 타입스크립트, Node.js, React, Angular, Vue.js 에서 사용할 수 있다.

SuperTest 는 superagent 라이브러리를 기반으로 하는 HTTP 테스트 라이브러리로 엔드포인트로 호출하는 것과 같은 E2E 테스트를 작성할 수 있다.

E2E(End-to-End) 테스트
사용자의 행동을 코드로 작성한 것


3. Jest Unit Test

Nest CLI 를 이용하여 프로젝트를 생성하면 기본 컴포넌트와 함께 해당 컴포넌트에 대한 테스트 파일이 생성된다.

$  nest new test --dry-run  # 실제 파일은 생성하지 않고 cli 로 출력만 해줌
⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? npm
CREATE test/.eslintrc.js (663 bytes)
CREATE test/.prettierrc (51 bytes)
CREATE test/README.md (3340 bytes)
CREATE test/nest-cli.json (171 bytes)
CREATE test/package.json (1935 bytes)
CREATE test/tsconfig.build.json (97 bytes)
CREATE test/tsconfig.json (546 bytes)
CREATE test/src/app.controller.spec.ts (617 bytes)
CREATE test/src/app.controller.ts (274 bytes)
CREATE test/src/app.module.ts (249 bytes)
CREATE test/src/app.service.ts (142 bytes)
CREATE test/src/main.ts (208 bytes)
CREATE test/test/app.e2e-spec.ts (630 bytes)
CREATE test/test/jest-e2e.json (183 bytes)
Dry run enabled. No files written to disk.


Command has been executed in dry run mode, nothing changed!

위에 보면 app.controller.spec.ts 파일이 자동으로 생성된 테스트 코드이며, 이렇게 자동으로 생성된 테스트 코드는 Jest 를 이용한다.

테스트 코드의 파일명은 .spec.ts 로 끝나야 하며, 그 규칙은 package.json 에 정의되어 있다. 나머지 옵션들은 Jest Configuration Docs 를 참고하세요.

package.json

{
  ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",  # 테스트 코드 파일의 확장자 형식 정의
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}

테스트 코드는 describe()it() 구문으로 구성된다.

  • describe()
    • test suite 를 작성하는 블록
    • test suite 란 테스트들을 의미있는 단위로 묶은 것
    • test suite 를 모아서 더 큰 단위의 test suite 만들 수 있음
    • test suite 는 테스트 수행에 필요한 환경 설정, 공통 모듈 생성 등과 같이 세부 test case 가 구행되기 위한 기반을 마련함
  • it()
    • 특정 테스트 시나리오 작성
    • 각 it() 구문은 별개의 test case 로 다뤄져야 하며, 서로 의존관계가 없어야 함

test case 작성법은 TDD 혹은 BDD (Behavior-Driven Development) 스타일로 할 수 있는데 여기선 Given / When / Then BDD 스타일로 테스트 코드를 작성해본다.

  • Given
    • 해당 test case 가 동작하기 위해 갖춰져야 하는 선행 조건 (=어떤 상황이 주어졌을 때)
  • When
    • 테스트하고자 하는 대상 코드 실행 (=대상 코드가 동작한다면)
  • Then
    • 대상 코드의 수행 결과 판단 (=기대한 값과 수행 결과가 맞는지)

describe() 의 첫 번째 인수는 문자열로 test suite 와 test case 의 이름이고, 두 번째 인수는 수행된 코드가 포함된 콜백 함수이다.

describe('UserService', () => {
    const userService: UserService = new UserService();
    
    describe('create', () => {
        it('should create user', () => {
            // Given
            ...
            // When
            ...
            // Then
            ...
        });

        it('should throw error when user already exists', () => {
          // Given
          ...
          // When
          ...
          // Then
          ...
        });
    });
});

describe()it() 외에 SetupTearDown 개념이 있다.

  • SetUp
    • test suite 내에서 모든 test case 를 수행하기 전에 수행되어야 하는 선행 조건 실행 (=반복 작업 줄임)
  • TearDown
    • 테스트 후 후처리 공통 처리

Jest 는 아래 4가지 구문을 제공한다.

  • beforeAll()
    • test suite 내의 모든 test case 수행 전 한번만 실행
  • beforeEach()
    • 각 test case 가 수행되기 전마다 수행
  • afterAll()
    • 모든 test case 가 수행된 후 한번만 실행
  • afterEach()
    • 각 테스트가 수행된 후마다 수행

위에 잠깐 테스트 프레임워크의 구성 요소를 설명하면서 Test Double 에 대해 언급했다.
Test Double 은 외부 모듈을 임의의 객체로 다루는 것으로 Dummy, Fake, Stub, Spy, Mock 으로 나뉜다.

  • Dummy
    • 테스트를 위해 생성된 가짜 데이터
    • 일반적으로 매개변수 목록을 채우는 데에만 사용됨
  • Fake
    • DB 에 있는 데이터를 테스트한다고 할 때 실제 DB 사용 시 I/O 에 많은 비용과 시간이 소요되므로 인메모리 DB 와 같이 메모리에 데이터를 적재해서 속도 개선
    • prod 환경에서는 테스트 수행 도중 시스템이 비정상 종료되면 잘못된 데이터가 남게 되므로, 잘못된 데이터가 남아도 상관없는 세션 등과 같은 것을 대상으로 테스트할 때 사용
  • Spy
    • 테스트 수행 정보 기록
    • 테스트 도중 함수 호출에 대해 해당 함수로 전달된 매개변수, 리턴값, 예외, 함수를 몇 번 호출했는지와 같은 정보 기록
  • Stub
    • 함수 호출 결과를 미리 준비한 응답으로 제공
  • Mock
    • Stub 과 비슷한 역할
    • 테스트 대상이 의존하는 대상의 행위에 대해 검증이 필요하면 Mock 사용, 상태에 대해 검증이 필요하면 Stub 사용
      (Mocks Aren’t Stubs 참고)

4. 유저 서비스

domain, application, interface, infrastructure layer 4개의 레이어로 구성된 각각의 컴포넌트에 테스트 코드를 작성해보자.

4개의 레이어는 NestJS - 클린 아키텍처 를 참고하세요.


4.1. domain layer

가장 내부원인 domain 객체에 대한 테스트 코드를 작성한다. User 도메인 객체와 User 도메인 객체를 생성하는 UserFactory 가 있는데 User 도메인 객체는 생성자, 게터, 세터만 존재하기 때문에 test case 작성할 필요가 없다. 만일 test coverage 퍼센트 충족 규칙이 있다면 작성할 수도 있겠지만 불필요한 리소스이므로 굳이 하지 않는 것이 좋다.

test coverage
테스트 코드가 수행될 때 전체 시스템의 소스 코드 또는 모듈을 수행하는 정도를 퍼센트로 표시

/src/users/domain/user.factory.ts

import { Injectable } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
import { User } from './user';
import { UserCreateEvent } from './user-create.event';

@Injectable()
export class UserFactory {
  constructor(private eventBus: EventBus) {}

  // 유저 객체 생성
  create(
    id: string,
    name: string,
    email: string,
    password: string,
    signupVerifyToken: string,
  ): User {
    // User 객체 생성 후 UserCreatedEvent 발행함, 이후 생성한 유저 도메인 객체 리턴
    const user = new User(id, name, email, password, signupVerifyToken);
    this.eventBus.publish(new UserCreateEvent(email, signupVerifyToken));
    return user;
  }

  // 이벤트 발행없이 유저 객체만 생성
  reconstitute(
    id: string,
    name: string,
    email: string,
    signupVerifyToken: string,
    password: string,
  ): User {
    return new User(id, name, email, signupVerifyToken, password);
  }
}

UserFactory 는 2개의 함수가 있는데 각각의 함수를 테스트하는 test suite 와 test case 뼈대를 작성한다.

/src/users/domain/user.factory.spec.ts

import { UserFactory } from './user.factory';

describe('UserFactory', () => {
  let userFactory: UserFactory; // test suite 전체에서 사용할 UserFactory

  describe('create', () => {
    it('should create user', () => {
      // Given
      // When
      // Then
    });
  });

  describe('reconstitute', () => {
    it('should reconstitute user', () => {
      // Given
      // When
      // Then
    });
  });
});

이제 test case 를 작성하기 위해 테스트 대상인 UserFactory 객체가 필요하므로 @nest/testing 패키지에서 제공하는 Test 클래스를 이용하여 테스트용 객체를 생성한다.

/src/users/domain/user.factory.spec.ts

import { UserFactory } from './user.factory';
import { Test } from '@nestjs/testing';

describe('UserFactory', () => {
  let userFactory: UserFactory; // test suite 전체에서 사용할 UserFactory

  // Test.createTestingModule() 함수를 이용하여 테스트 모듈 생성
  // 함수의 인수가 ModuleMetadata 이므로 모듈을 임포트할때와 동일하게 컴포넌트 가져올 수 있음
  // UserFactory 가 대상 클래스이므로 이 모듈을 프로바이더로 가져옴
  // 모듈을 가져오는 것은 전체 test suite 내에서 한 번만 이루어지므로 설정 단계인 beforeAll() 구문 내에서 수행
  // Test.createTestingModule() 의 리턴값은 TestingModuleBuilder 임, compile 함수를 수행하여 모듈 생성을 완료한다. (이 함수는 비동기로 처리됨)
  beforeAll(async () => {
    const module = await Test.createTestingModule({
      providers: [UserFactory],
    }).compile();

    userFactory = module.get(UserFactory); // 프로바이더로 제공된 UserFactory 객체를 테스트 모듈에서 가져옴
  });
  
  describe('create', () => {
    ...
  });

  describe('reconstitute', () => {
    ...
  });
});

UserFactory 의 create 함수 구현을 보자.

/src/users/domain/user.factory.ts

// 유저 객체 생성
create(
  id: string,
  name: string,
  email: string,
  password: string,
  signupVerifyToken: string,
): User {
  // User 객체 생성 후 UserCreatedEvent 발행함, 이후 생성한 유저 도메인 객체 리턴
  const user = new User(id, name, email, password, signupVerifyToken);
  this.eventBus.publish(new UserCreateEvent(email, signupVerifyToken));
  return user;
}

User 도메인 객체를 생성하는 과정에서 UserCreateEvent 이벤트를 발행하고 있는데 이 동작은 UserFactory 를 테스트하는데 영향을 미치면 안되므로 이벤트 버스를 통해 제대로 전송이 되었다고 가정하고, 이벤트 발송 함수인 publish 가 호출되었는지를 Spy 를 통해 판별한다.

EventBus 객체도 테스트를 위해 필요하므로 Mock 객체로 선언한다.

/src/users/domain/user.factory.spec.ts

...
import { Test } from '@nestjs/testing';
import { EventBus } from '@nestjs/cqrs';

describe('UserFactory', () => {
  ...
  let eventBus: jest.Mocked<EventBus>; // Jest 에서 제공하는 Mocked 객체로 EventBus 선언

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      providers: [
        ...
        // EventBus 를 프로바이더로 제공
        // 이 때 EventBus 의 함수를 mocking 함
        // publish 함수가 jest.fn() 으로 선언되었는데 이는 어떠한 동작도 하지 않는 함수라는 의미
        {
          provide: EventBus,
          useValue: {
            publish: jest.fn(),
          },
        },
      ],
    }).compile();

    ...
    eventBus = module.get(EventBus); // 프로바이더로 제공된 EventBus 객체를 테스트 모듈에서 가져옴
  });

  describe('create', () => {
    ...
  });

  describe('reconstitute', () => {
    ...
  });
});

이제 create 함수의 test case 를 작성한다.

/src/users/domain/user.factory.spec.ts

...
import { User } from './user';

describe('UserFactory', () => {
  ...

  describe('create', () => {
    it('should create user', () => {
      // Given
      // 주어진 조건은 딱히 없으므로 작성하지 않음

      // When
      // create 함수 실행
      const user = userFactory.create(
        'user-id',
        'assu3',
        'test3@test.com',
        'password1234',
        'signup-verify-token',
      );

      // Then
      // 수행 결과가 원하는 결과와 맞는지 검증
      // When 단계 수행 시 원하는 결과를 선언하고 Jest 에서 제공하는 Matcher 를 이용하여 판단
      const expected = new User(
        'user-id',
        'assu3',
        'test3@test.com',
        'password1234',
        'signup-verify-token',
      );

      // UserFactory.create 를 통해 생성한 User 객체가 원하는 객체와 맞는지 검사
      expect(expected).toEqual(user);

      // EventBus.publish 함수가 한번 호출되었는지 판단
      expect(eventBus.publish).toBeCalledTimes(1);
    });
  });
...
});

아래는 reconstitute 함수의 test case 까지 작성한 전체 테스트 코드이다.

/src/users/domain/user.factory.spec.ts

import { UserFactory } from './user.factory';
import { Test } from '@nestjs/testing';
import { EventBus } from '@nestjs/cqrs';
import { User } from './user';

describe('UserFactoryTest', () => {
  let userFactory: UserFactory; // test suite 전체에서 사용할 UserFactory
  let eventBus: jest.Mocked<EventBus>; // Jest 에서 제공하는 Mocked 객체로 EventBus 선언

  // Test.createTestingModule() 함수를 이용하여 테스트 모듈 생성
  // 함수의 인수가 ModuleMetadata 이므로 모듈을 임포트할때와 동일하게 컴포넌트 가져올 수 있음
  // UserFactory 가 대상 클래스이므로 이 모듈을 프로바이더로 가져옴
  // 모듈을 가져오는 것은 전체 test suite 내에서 한 번만 이루어지므로 설정 단계인 beforeAll() 구문 내에서 수행
  // Test.createTestingModule() 의 리턴값은 TestingModuleBuilder 임, compile 함수를 수행하여 모듈 생성을 완료한다. (이 함수는 비동기로 처리됨)
  beforeAll(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UserFactory,
        // EventBus 를 프로바이더로 제공
        // 이 때 EventBus 의 함수를 mocking 함
        // publish 함수가 jest.fn() 으로 선언되었는데 이는 어떠한 동작도 하지 않는 함수라는 의미
        {
          provide: EventBus,
          useValue: {
            publish: jest.fn(),
          },
        },
      ],
    }).compile();

    userFactory = module.get(UserFactory); // 프로바이더로 제공된 UserFactory 객체를 테스트 모듈에서 가져옴
    eventBus = module.get(EventBus); // 프로바이더로 제공된 EventBus 객체를 테스트 모듈에서 가져옴
  });

  describe('createTest', () => {
    it('should create user', () => {
      // Given
      // 주어진 조건은 딱히 없으므로 작성하지 않음

      // When
      // create 함수 실행
      const user = userFactory.create(
              'user-id',
              'assu3',
              'test3@test.com',
              'password1234',
              'signup-verify-token',
      );

      // Then
      // 수행 결과가 원하는 결과와 맞는지 검증
      // When 단계 수행 시 원하는 결과를 선언하고 Jest 에서 제공하는 Matcher 를 이용하여 판단
      const expected = new User(
              'user-id',
              'assu3',
              'test3@test.com',
              'password1234',
              'signup-verify-token',
      );

      // UserFactory.create 를 통해 생성한 User 객체가 원하는 객체와 맞는지 검사
      expect(expected).toEqual(user);

      // EventBus.publish 함수가 한번 호출되었는지 판단
      expect(eventBus.publish).toBeCalledTimes(1);
    });
  });

  describe('reconstituteTest', () => {
    it('should reconstitute user', () => {
      // Given
      // 주어진 조건은 딱히 없으므로 작성하지 않음

      // When
      const user = userFactory.reconstitute(
              'user-id',
              'assu3',
              'test3@test.com',
              'password1234',
              'signup-verify-token',
      );

      // Then
      const expected = new User(
              'user-id',
              'assu3',
              'test3@test.com',
              'password1234',
              'signup-verify-token',
      );

      // UserFactory.create 를 통해 생성한 User 객체가 원하는 객체와 맞는지 검사
      expect(expected).toEqual(user);
    });
  });
});

이제 테스트를 수행해본다.

$ npm run test

> user-service@0.0.1 test
> jest

 PASS  src/users/domain/user.factory.spec.ts
  UserFactoryTest
    createTest
      ✓ should create user (2 ms)
    reconstituteTest
      ✓ should reconstitute user

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.876 s, estimated 4 s
Ran all test suites.

4.2. application layer

application layer 의 코드 중 여기선 CreateUserHandler 만 테스트 코드를 작성해본다.

$ tree -L 4 -N -I "node_modules"

├── application
│   ├── adapter
│   │   └── iemail.service.ts
│   ├── command
│   │   ├── create-user.command.ts
│   │   ├── create-user.handler.ts
│   │   ├── login.command.ts
│   │   ├── login.handler.ts
│   │   ├── verify-email.command.ts
│   │   └── verify-email.handler.ts
│   ├── event
│   │   └── user-event.handler.ts
│   └── query
│       ├── get-user-info.handler.ts
│       └── get-user-info.query.ts

/src/users/application/command/create-user.handler.spec.ts

import * as uuid from 'uuid';
import * as ulid from 'ulid';
import { UserRepository } from 'src/users/infra/db/repository/UserRepository';
import { CreateUserHandler } from './create-user.handler';
import { Test } from '@nestjs/testing';
import { CreateUserCommand } from './create-user.command';
import { UserFactory } from '../../domain/user.factory';
import { UnprocessableEntityException } from '@nestjs/common';

// CreateUserHandler.execute() 에서 uuid, ulid 사용
// 외부 라이브러리가 생성하는 임의의 문자열이 항상 값은 값인 '0000-0000-0000-0000', 'ulid' 를 리턴하도록 함
jest.mock('uuid');
jest.mock('ulid');
jest.spyOn(uuid, 'v1').mockReturnValue('0000-0000-0000-0000');
jest.spyOn(ulid, 'ulid').mockReturnValue('ulid');

describe('CreateUserHandlerTest', () => {
  // 테스트 대상인 CreateUserHandler 와 의존하고 있는 클래스 선언
  let createUserHandler: CreateUserHandler;
  let userFactory: UserFactory;
  let userRepository: UserRepository;

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      providers: [
        CreateUserHandler,
        // UserFactory, UserRepository 를 mock 객체로 제공
        {
          provide: UserFactory,
          useValue: {
            create: jest.fn(),
          },
        },
        {
          provide: 'UserRepository',
          useValue: {
            save: jest.fn(),
          },
        },
      ],
    }).compile();

    createUserHandler = module.get(CreateUserHandler);
    userFactory = module.get(UserFactory);
    userRepository = module.get('UserRepository');
  });

  // 항상 같은 값을 갖는 변수를 미리 선언하고 재사용하도록 함
  const id = ulid.ulid();
  const name = 'assu';
  const email = 'test3@test.com';
  const password = 'password1234';
  const signupVerifyToken = uuid.v1();

  describe('executeTest', () => {
    it('should execute CreateUserHandler', async () => {
      // Given
      // userRepository 에 저장된 유저가 없는 조건 설정
      userRepository.findByEmail = jest.fn().mockResolvedValue(null);

      // When
      // execute 함수 실행
      await createUserHandler.execute(
        new CreateUserCommand(name, email, password),
      );

      // Then
      // 수행 결과가 원하는 결과와 맞는지 검증
      // When 단계 수행 시 원하는 결과를 선언하고 Jest 에서 제공하는 Matcher 를 이용하여 판단
      // UserFactory 에서는 테스트 대상 클래스가 의존하고 있는 객체의 함수를 단순히 호출하는지만 검증했다면 이번엔 인수까지 제대로 넘기고 있는지 검증
      expect(userRepository.save).toBeCalledWith(
        id,
        name,
        email,
        password,
        signupVerifyToken,
      );

      expect(userFactory.create).toBeCalledWith(
        id,
        name,
        email,
        password,
        signupVerifyToken,
      );
    });

    // UserRepository 에 유저 정보가 있을 경우 test case
    it('should throw UnprocessableEntityException when user exists', async () => {
      // Given
      // 생성하려는 유저 정보가 이미 있는 경우를 mocking
      userRepository.findByEmail = jest.fn().mockResolvedValue({
        id,
        name,
        email,
        password,
        signupVerifyToken,
      });

      // When

      // Then
      // 원하는 예외가 발생하는가?
      await expect(
        createUserHandler.execute(new CreateUserCommand(name, email, password)),
      ).rejects.toThrowError(UnprocessableEntityException);
    });
  });
});
$ npm run test

> user-service@0.0.1 test
> jest

 PASS  src/users/domain/user.factory.spec.ts
 PASS  src/users/application/command/create-user.handler.spec.ts

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        3.98 s
Ran all test suites.


5. Test Coverage 측정

Jest 로 테스트 시 Test Coverage 를 측정할 수 있다.

$ npm run test:cov

> user-service@0.0.1 test:cov
> jest --coverage

 PASS  src/users/domain/user.factory.spec.ts
 PASS  src/users/application/command/create-user.handler.spec.ts
-----------------------------------|---------|----------|---------|---------|-------------------
File                               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------------------|---------|----------|---------|---------|-------------------
All files                          |   12.24 |     6.25 |   13.23 |    12.6 |                   
 src                               |       0 |        0 |       0 |       0 |                   
  app.module.ts                    |       0 |        0 |     100 |       0 | 1-64              
  auth.guard.ts                    |       0 |      100 |       0 |       0 | 1-21              
  main.ts                          |       0 |      100 |       0 |       0 | 1-20              
 src/auth                          |       0 |      100 |       0 |       0 |                   
  auth.module.ts                   |       0 |      100 |     100 |       0 | 1-8               
  auth.service.ts                  |       0 |      100 |       0 |       0 | 1-49              
 src/config                        |       0 |      100 |       0 |       0 |                   
  authConfig.ts                    |       0 |      100 |       0 |       0 | 1-3               
  emailConfig.ts                   |       0 |      100 |       0 |       0 | 1-4               
  validationSchema.ts              |       0 |      100 |     100 |       0 | 1-3               
 src/email                         |       0 |      100 |       0 |       0 |                   
  email.module.ts                  |       0 |      100 |     100 |       0 | 1-5               
  email.service.ts                 |       0 |      100 |       0 |       0 | 1-52              
 src/exception                     |       0 |        0 |       0 |       0 |                   
  exception.module.ts              |       0 |      100 |     100 |       0 | 1-14              
  http-exception.filter.ts         |       0 |        0 |       0 |       0 | 1-37              
 src/health-check                  |       0 |        0 |       0 |       0 |                   
  dog.health.ts                    |       0 |        0 |       0 |       0 | 1-28              
  health-check.controller.ts       |       0 |      100 |       0 |       0 | 1-31              
 src/logging                       |       0 |      100 |       0 |       0 |                   
  logging.interceptor.ts           |       0 |      100 |       0 |       0 | 1-26              
  logging.module.ts                |       0 |      100 |     100 |       0 | 1-11              
 src/migrations                    |       0 |      100 |       0 |       0 |                   
  1680769589673-CreateUserTable.ts |       0 |      100 |       0 |       0 | 3-13              
 src/users                         |       0 |      100 |     100 |       0 |                   
  users.module.ts                  |       0 |      100 |     100 |       0 | 1-46              
 src/users/application/command     |   41.07 |    33.33 |   33.33 |   42.85 |                   
  create-user.command.ts           |     100 |      100 |     100 |     100 |                   
  create-user.handler.ts           |     100 |      100 |     100 |     100 |                   
  login.command.ts                 |       0 |      100 |       0 |       0 | 3-4               
  login.handler.ts                 |       0 |        0 |       0 |       0 | 1-31              
  verify-email.command.ts          |       0 |      100 |       0 |       0 | 3-4               
  verify-email.handler.ts          |       0 |        0 |       0 |       0 | 1-31              
 src/users/application/event       |       0 |        0 |       0 |       0 |                   
  user-event.handler.ts            |       0 |        0 |       0 |       0 | 1-23              
 src/users/application/query       |       0 |        0 |       0 |       0 |                   
  get-user-info.handler.ts         |       0 |        0 |       0 |       0 | 1-31              
  get-user-info.query.ts           |       0 |      100 |       0 |       0 | 3-4               
 src/users/domain                  |   89.28 |      100 |   66.66 |      88 |                   
  cqrs-event.ts                    |     100 |      100 |     100 |     100 |                   
  user-create.event.ts             |     100 |      100 |     100 |     100 |                   
  user.factory.ts                  |     100 |      100 |     100 |     100 |                   
  user.ts                          |   66.66 |      100 |      25 |   66.66 | 11-19             
 src/users/event                   |       0 |      100 |       0 |       0 |                   
  test.event.ts                    |       0 |      100 |       0 |       0 | 1-7               
 src/users/infra/adapter           |       0 |      100 |       0 |       0 |                   
  email.service.ts                 |       0 |      100 |       0 |       0 | 2-13              
 src/users/infra/db/entity         |       0 |      100 |     100 |       0 |                   
  user.entity.ts                   |       0 |      100 |     100 |       0 | 1-17              
 src/users/infra/db/repository     |       0 |        0 |       0 |       0 |                   
  UserRepository.ts                |       0 |        0 |       0 |       0 | 3-97              
 src/users/interface               |       0 |      100 |       0 |       0 |                   
  users.controller.ts              |       0 |      100 |       0 |       0 | 1-133             
 src/users/interface/dto           |       0 |      100 |       0 |       0 |                   
  create-user.dto.ts               |       0 |      100 |       0 |       0 | 1-25              
  user-login.dto.ts                |       0 |      100 |     100 |       0 | 1                 
  verify-email.dto.ts              |       0 |      100 |     100 |       0 | 1                 
 src/utils/decorators              |       0 |        0 |       0 |       0 |                   
  not-in.ts                        |       0 |        0 |       0 |       0 | 1-29              
-----------------------------------|---------|----------|---------|---------|-------------------

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        16.847 s
Ran all test suites.

테스트가 애플리케이션 코드의 어느 정도를 테스트하고 있는지 확인 가능하기 때문에 사내 테스트 커버리지 기준에 만족하지 못할 경우 배포되지 않도록 CI/CD 과정에 포함시킬 수 있다.

테스트 커버리지 측정 수행 후에 root 아래에 coverage 디렉터리가 생기는데 여기에 측정 결과가 저장되어 있다.
아래와 같이 HTML 로 리포트를 확인할 수 있다.

Test Coverage Test Coverage Test Coverage


참고 사이트 & 함께 보면 좋은 사이트

본 포스트는 한용재 저자의 NestJS로 배우는 백엔드 프로그래밍을 기반으로 스터디하며 정리한 내용들입니다.






© 2020.08. by assu10

Powered by assu10