본문 바로가기

카테고리 없음

Nest.js 의 유닛 테스트(Unit test)

들어가며

소프트웨어 개발 과정에서 코드를 작성하고 개발자가 직접 몇개의 인풋을 넣어보면서 자기가 알맞게 코딩했는지 확인해볼텐데요. 이렇게 개발자가 직접 몇 개의 값만 확인해보고 넘어가는 방법은 몇가지 문제가 있을 수 있습니다.

  • 확인해보고 싶은 값을 매 테스트마다 다시 입력해야한다.
  • 기능이 추가될 때마다 입력해야 할 값이 점점 늘어난다.
  • 다른사람이 내 모듈을 사용하거나 내 모듈을 수정해야 할 경우 내가 고려했던 경우의 수까지 모두 테스트해봐야 한다.

 이렇게 테스트코드를 작성해 두지 않으면 앞서 작성해 둔 코드들이 제대로 동작하는지 장담할 수 없고 품질에 큰 문제를 야기할 수 있습니다. 그럼 유닛테스트의 장점은 무엇이 있을까요

  • 다양한 상황에서 코드를 실행해 볼 수 있다.
  • 같은 결과물인지 확인하기가 용이하므로 코드 내부를 수정하기가(리팩토링) 쉽다.

이제 테스트코드가 필요하다는 점은 어느정도 공감할 수 있을 텐데요. 이번 포스트에서는 Nest.js framework에서의 유닛테스트작성에 대해서 살펴보겠습니다.

Unit test

우선 유닛 테스트가 무엇인지에 대해서 알아보겠습니다. 소프트웨어를 테스팅은 테스팅 대상의 범위나 크기에 따라 integration test, end-to-end test, unit test등으로 나눌 수 있습니다. 이 중에서도 유닛 테스트는 소프트웨어를 이루고 있는 가장 작은 모듈에 대해서 테스트하는 것을 말합니다. 보통 클래스 단위로 테스트를 하는 경우가 많지만 함수 단위로 테스트를 할 수도 있고 너무 크지 않은 범위에서 적절하게 정할 수도 있습니다. 유닛 테스트를 작성할 때 중요한 점은 각각의 테스트가 다른 테스트의 결과에 의존하면 안된다는 것인데요. 각 테스트가 독립적인 목표와 상태를 가지고 있어서 다른 테스트가 실패하든 성공하든 영향을 받지 않아야 합니다. 그래야 테스트가 실패했을때 어느 부분에서 버그가 발생한 것인지 쉽게 찾을수 있겠죠. 각 언어별로 유닛테스트를 자동화하거나 유닛테스트 작성을 도와주는 도구들을 적절하게 활용하는 것도 좋습니다.

Nest.js의 테스트

Nestjs에서는 javascript 테스트 프레임워크인 jest를 기본 테스트 프레임워크로 지원하고 있습니다. 먼저 nestjs testing 패키지부터 설치하겠습니다.

yarn add @nestjs/testing --dev # 또는
npm i --save-dev @nestjs/testing

아래는 Nestjs에서 App을 생성했을때 함께 생성되는 spec.ts파일입니다.  

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

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

  describe('root', () => {
    it('should return "Hello World!!!"', () => {
      expect(appController.getHello()).toBe('Hello World!!!!');
    });
  });
});

jestjs에서 테스트 케이스들은 describe 내에 정의를 하게 됩니다.  위의 파일의 가장 밖의 describe에 'AppController'라고 적어 이 내부에 정의할 테스트케이스가 AppController에 대한 테스트 라는 것을 나타냅니다. beforeEach는 각 테스트케이스가 실행되기 이전에 수행될 내용을 정의해두었습니다. 여기서는 AppController와 AppService를 이용하여 테스팅모듈을 생성하고 appController를 만들어 두었네요. 

// 위의 코드의 아랫부분
describe('root', () => {
    it('should return "Hello World!!!"', () => {
      expect(appController.getHello()).toBe('Hello World!!!!');
    });
  });

그 아래에는 다시 describe를 선언하고 'root'라고 표시해두어 AppController의 root에 대한 테스트를 할 것임을 나타내었습니다. 그 아랫줄에 보이는 it에서 테스트 케이스에 대한 설명과 진짜 테스트 코드를 작성합니다. it('shold return "Hello World", ()=>{}) 와 같이 이 테스트 케이스는 어떤 결과를 보여야 한다는 식으로 타겟 코드의 기대 동작을 적어주면 됩니다. 그리고 그 안에는 appController.getHello()를 콜하고 그 결과를 기대 결과('Hello World!!!!')와 비교하였습니다. 

이제 yarn test하여 테스트 코드를 실행해보겠습니다.

getHello()가 "Hello World!!!!"를 리턴하여 성공하였습니다.

만약 getHello()의 리턴이 'Hello World!!!!' 가 아니라면 아래와 같이 실패하게 됩니다.

Mock 이용하기

앞에서는 문자열을 리턴하는 아주 간단한 예제를 살펴보았습니다. 이번에는 데이터베이스에서 데이터를 조회하는 함수를 테스트해보겠습니다. 이렇게 데이터 베이스를 조회하는 함수의 경우 데이터베이스에 데이터가 하나도 없는 상황이나 특정 데이터를 뽑아올 수 있는지에 대한 상황 등을 적절하게 처리할 수 있나 테스트 해 보아야 합니다. 이 때 데이터베이스에 직접 데이터를 조작하여 테스트 환경을 만드는것은 비효율적이며 유닛테스트라는 이름에도 어울리지 않습니다. 이럴때 Mock이라는 개념을 활용하면 되는데요. 내가 테스트할 대상이 활용하는 모듈들을 내가 만든 Mock객체로 바꾸어 여러 테스트 상황을 만들고 테스트 하는 것입니다. 앞으로 살펴볼 예제에서는 데이터베이스와 상호작용하는 UserRepository를 MockUserRepository로 바꾸고 각 상황에 맞게 MockUserRepository가 동작하도록 만든 뒤에 UserService를 테스트 할 것입니다. 

우선 users.entity.ts를 정의해줍니다. typeorm을 사용하였습니다.

//users.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  email: string;

  @Column()
  password: string;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}

다음은 users.service.ts입니다. 간단하게 user 한명을 조회해오고 유저 id가 1이면 에러를 던지는 로직입니다.

//users.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './users.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User) private usersRepository: Repository<User>,
  ) {}

  async findOneById(id: number): Promise<User> {
    let user: User = null;

    user = await this.usersRepository.findOne(id);

    if (user.id === 1) {
      throw new InternalServerErrorException('정지 당한 유저입니다.');
    }
    return user;
  }
}

이제 user.service.spec.ts를 작성하여 UserService의 findOneById가 온전하게 동작하는지 테스트해보겠습니다. 

import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';

describe('UsersService', () => {
  let service: UsersService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UsersService],
    }).compile();

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

  describe('findOneById', () => {
    it.todo('should return one user who has id in input param');
    it.todo('should return InternelServerException when input userId is 1');
  })
});

우선 describe에 간단히 테스팅 모듈을 만들어주고 it.todo를 이용하여 작성해야할 테스트에 대해서 명시해줍니다. 이번 예시에서는 두가지 테스트를 작성하겠습니다. 하나는 정상적으로 유저를 조회하여 리턴하는 경우와 1번 id를 가진유저에 대한 조회를 시도하여 에러를 리턴하는 경우입니다. 본격적으로 테스트 케이스를 작성하기 전에 이번 예제에서는 타겟 코드에서 DB에 접근하므로 이부분을 Mock으로 처리해주겠습니다.

import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './users.entity';
import { UsersService } from './users.service';

class MockRepository {
  async findOne(id) {
    const user: User = new User();
    user.id = id;
    return user;
  }
}

describe('UsersService', () => {
  let service: UsersService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useClass: MockRepository,
        },
      ],
    }).compile();

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

  it.todo('should return one user who has id in input param');
  it.todo('should return InternelServerException when input userId is 1');
});

Nestjs에서 테스트 모듈의 providers를 정해줄때 위와 같이 Repository를 MockRepository로 변경해 줄 수 있습니다. MockRepository는 우리가 테스트할 상황에 맞게 정의되어 있습니다. 이제 service.findOneById 안의 usersRepository.findOne은 MockRepository의 findOne을 호출할 것입니다.

import { InternalServerErrorException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './users.entity';
import { UsersService } from './users.service';

class MockRepository {
  async findOne(id) {
    const user: User = new User();
    user.id = id;
    return user;
  }
}

describe('UsersService', () => {
  let service: UsersService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useClass: MockRepository,
        },
      ],
    }).compile();

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

  it('should return one user who has id in input param', async () => {
    const userId = 42;
    const result = await service.findOneById(userId);
    expect(result.id).toBe(userId);
  });

  it('should return InternelServerException when input userId is 1', async () => {
    const userId = 1;

    await expect(service.findOneById(userId)).rejects.toThrow(
      InternalServerErrorException,
    );
  });
});

ㄹ이제 각 테스트 케이스에 대해서 테스트 코드를 작성하였습니다. 첫번째 테스트의 경우는 42번 유저를 조회한 결과로 얻은 유저의 id가 42인지를 확인할 것이고 두번째 경우는 1번 유저를 조회하였을때 정의해둔 에러가 throw되는지 확인하게 됩니다. 이제 yarn test로 두가지 테스트 케이스를 실행합니다.

 

 

두 가지 테스트 케이스를 모두 통과하였습니다. 이제 1번 유저에 대한 예외 상황에 대한 구현과 특정 번호에 대한 유저 조회는 제대로 동작한다고 볼 수 있습니다.

 

마치며

이번 포스트에서는 Unittest의 개념과 Nestjs 및 jestjs로 이를 구현하는 방법에 대해서 정리하였습니다. 이번 포스트에서는 리턴 값을 확인하거나 에러가 발생하는지를 확인하는 방식으로 각 함수의 결과를 확인하였습니다. 이외에도 jest.spyOn을 이용하여 내부에서 호출되는 함수가 정말 호출되었는지 확인하거나 출력값을 확인하는 방식으로 테스트 대상이 제대로 구현되었는지를 확인 할 수도 있습니다. 한편 소프트웨어 개발 방법론에는 TDD(Test driven development)라고 하는 소프트웨어 스펙에 맞는 테스트 코드를 먼저 작성하고, 비즈니스 로직에 해당하는 코드를 작성하는 방식도 있습니다. 이처럼 테스트코드는 소프트웨어 개발에서 중요한 주제이기 때문에 그 필요성에 대해서 생각해보고 적절하게 테스트를 작성해야 할 것입니다.

 

reference

(jestjs)[https://jestjs.io/docs/en/getting-started]

[https://en.wikipedia.org/wiki/Unit_testing]
[https://docs.nestjs.com/fundamentals/testing]