Lsiron

Nest.js에서 유닛테스팅 해보기 본문

백엔드/Nest.js

Nest.js에서 유닛테스팅 해보기

Lsiron 2024. 8. 28. 06:14

movies.service.ts

//movie.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { Movie } from './entities/movie.entity';
import { CreateMovieDto } from './dto/create-movie.dto';
import { UpdateMovieDto } from './dto/update-movie.dto';

@Injectable()
export class MoviesService {
    private movies: Movie[] = [];

    getAll(): Movie[] {
        return this.movies;
    }

    getOne(id:number): Movie {
        const movie = this.movies.find(movie => movie.id === id);
        if(!movie) {
            throw new NotFoundException(`Movie with ID ${id} not found`)
        }
        
        return movie;
    }

    delete(id: number) {
        this.getOne(id)
        this.movies = this.movies.filter(movie => movie.id !== id);
    }

    create(movieData: CreateMovieDto){
        this.movies.push({
            id: this.movies.length + 1,
            ...movieData
        })
    }

    update(id: number, updateData: UpdateMovieDto) {
        const movie = this.getOne(id);
        this.delete(id);
        this.movies.push({...movie, ...updateData })
    }
}

movies.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';

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

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

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

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

 

spec.ts 파일에 대해 각각 자세하게 알아보고 넘어가도록 하자.

1. describe('MoviesService', () => { ... });

describe() 함수는 Jest의 블록 함수로, 테스트 스위트를 정의하는 데 사용된다.

 

테스트 스위트(Test Suite)는 여러 개의 개별적인 테스트 케이스를 묶어 놓은 집합을 말한다.

 

여기서는 'MoviesService'라는 이름의 테스트 그룹을 정의했다. 이 그룹 내에서 해당 서비스의 다양한 기능이나 메서드를 테스트할 수 있다.

2. let service: MoviesService;

service 변수는 MoviesService의 인스턴스를 참조하기 위한 변수이다.

 

이 변수를 통해 테스트 내에서 MoviesService의 메서드를 호출하거나 상태를 확인할 수 있다.

3. beforeEach(async () => { ... });

beforeEach() 함수는 각 테스트 케이스가 실행되기 전에 실행되는 함수이다.

 

여기서 각 테스트 실행 전 MoviesService의 인스턴스를 생성하는 로직이 포함된다. async 함수로 정의된 이유는 Test.createTestingModule()이 비동기 작업이기 때문이다.

 

참고로 beforeEach() 말고도 afterEach(), afterAll() 과 beforeAll() 함수가 있다. 

 

afterEach() 함수는 각 테스트 케이스가 실행된 후에 실행되는 함수이다.

여기서는 테스트 후에 필요한 정리 작업(클린업)을 수행할 수 있다. 예를 들어, 테스트 중에 변경된 상태를 원래대로 되돌리거나, 테스트 리소스를 해제하는 로직을 포함할 수 있다.

 

beforeAll() 함수는 테스트 스위트 내의 모든 테스트 케이스가 실행되기 전에 단 한 번만 실행되는 함수이다.

여기서 테스트 환경을 초기화하거나, 테스트 전체에서 사용할 리소스를 준비하는 로직을 포함할 수 있다.

 

afterAll() 함수는 테스트 스위트 내의 모든 테스트 케이스가 완료된 후에 단 한 번만 실행되는 함수이다.

여기서는 전체 테스트가 끝난 후 리소스를 정리하거나, 초기화된 설정을 되돌리는 로직을 포함할 수 있다.

4. const module: TestingModule = await Test.createTestingModule({ ... }).compile();

Test.createTestingModule()은 Nest.js의 테스트 모듈을 생성하는 메서드이다.

 

이 메서드에 모듈의 설정을 넘기면, 해당 설정에 따라 테스트 환경을 구성한다.

 

providers: [MoviesService]: MoviesService를 프로바이더로 설정해 주입받을 수 있도록 한다. 이것은 테스트할 서비스가 실제 Nest.js 애플리케이션에서와 동일하게 동작하도록 하는 역할을 한다.

 

compile()은 테스트 모듈을 컴파일하고 준비하는 메서드로, 비동기 작업이기 때문에 await 키워드를 사용하여 완료될 때까지 기다린다.

5. service = module.get<MoviesService>(MoviesService);

module.get()은 TestingModule 내에서 특정 프로바이더를 가져오는 메서드이다.

 

여기서는 MoviesService 프로바이더를 가져와서 service 변수에 할당한다. 이로써 service 변수를 통해 MoviesService의 메서드에 접근할 수 있게 된다.

6. it('should be defined', () => { ... });

it() 함수는 실제 테스트 케이스를 정의하는 함수이다.

 

이 테스트 케이스에서는 'should be defined'라는 설명이 붙는다. 이 설명은 해당 테스트가 무엇을 검증하려는지 설명하는 역할을 한다.

 

테스트 내용: expect(service).toBeDefined();를 통해 service가 잘 정의되어 있는지 확인한다. MoviesService 인스턴스가 제대로 생성되었는지를 확인하는 기본적인 테스트이다.

7. expect(service).toBeDefined();

expect() 함수는 테스트에서 값이 기대하는 상태인지 검증하는 Jest의 핵심 함수이다.

 

toBeDefined()는 service 변수가 정의되었는지, 즉 undefined가 아닌지를 확인한다.

 

이 테스트는 단순히 MoviesService가 정상적으로 생성되었고, service 변수가 정의되었음을 검증한다.

 

한번 테스트 코드를 만들고 실행 해 보자.

 

먼저 아래 명령어를 입력하여, 자동으로 테스팅을 하도록 설정 해 주자.

$ npm run test:watch

 

spec.ts 파일에 아래와 같이 입력 해 보자.

import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';

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

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

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

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

  it('should be 4', () => {  // 테스팅 코드 추가
    expect(2+2).toEqual(5);
  });
});

 

입력 후, 저장을 하면 아래와 같이 콘솔 창에 출력된다. ( 자동으로 테스팅 되기 때문에 저장만 해도 출력된다. )

 

우리가 만든 테스트코드는 'should be 4' 이므로 테스팅이 Pass 된 것을 확인 할 수 있다.

 

그렇다면? toEqual 값에 4 말고 5를 넣은 뒤, 결과가 어떻게 나오는지 보자.

 

어느 부분이 잘못됐는지 자세하게 나온다. 

 

위와 같이 테스트를 하면 된다. 이제 본문 첫 단에 있는 movies.service.ts를 테스트 해 보자.

 

getAll() 함수를 먼저 테스트 해 보겠다.

 

getAll()이 배열을 반환하는지 확인 해 보자. 아래와 같이 코드를 작성 해 주자.

import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';

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

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

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

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

  describe('getAll', () => {
    const result = service.getAll()

    expect(result).toBeInstanceOf(Array);
  })
});

 

테스트가 성공적으로 실행 됐음을 알 수 있다. 이 말은 즉 getAll() 함수는 배열을 반환한다는 것이다.

 

이제 getOne() 함수를 테스트 해 보자. getAll() 함수를 테스트 했던 코드 아래에 아래와 같이 적어준다.

  describe("getOne", () => {
    it("should return a movie", () => {
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      const movie = service.getOne(1);
      expect(movie).toBeDefined();
      expect(movie.id).toEqual(1);
    });
  });

 

getOne() 함수를 테스트할 때 Movie가 create 되어 있지 않다면 문제가 될 수 있기 때문에, 먼저 getOne() 함수를 위한 movie를 생성 해 주고 테스트 코드를 작성 하였다.

 

테스트 결과는 movie를 반환하고 id는 1이어야하는 조건이 Pass 되었다. 테스트가 성공적으로 이루어졌다.

 

이제 getOne() 함수에서 에러가 발생하는지의 여부를 테스트 해 보자.

    it("should throw 404 error", () => {
      try{
        service.getOne(999);
      }catch(e){
        expect(e).toBeInstanceOf(NotFoundException);
        expect(e.message).toEqual("Movie with ID 999 not found")
      }
    })

 

getOne() 함수 인자에 말도 안 되는 ID값을 집어 넣었을 때 에러가 정상적으로 발생하는지 확인하기 위한 테스트 코드이다.

 

역시나 정상적으로 실행이 완료 되었으며, 404 에러를 던지는 조건과, 에러 메세지의 조건이 Pass 된 것을 볼 수 있다.

 

번외로 실수가 있을 경우, 아래와 같이 무엇이 잘못 됐는지 보여준다.

 

이제 delete() 함수에서 에러가 발생하는지의 여부를 테스트 해 보자. getOne() 함수를 테스트 했던 코드 아래에 아래와 같이 적어준다.

  describe('delete', () => {
    it('deletes a movie', () => {
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      const beforeDelete = service.getAll().length;
      service.delete(1);
      const afterDelete = service.getAll().length;
      expect(afterDelete).toBeLessThan(beforeDelete);
    });
    it('should return a 404', () => {
      try {
        service.delete(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });

 

똑같이 delete() 함수를 테스트할 때 Movie가 create 되어 있지 않다면 문제가 될 수 있기 때문에, 먼저 delete() 함수를 위한 movie를 생성 해 주고 테스트 코드를 작성 하였다.

 

한 개가 삭제됐기 때문에,  삭제 후의 Movies 배열의 길이가 삭제 전의 배열의 길이보다 작아야 한다는 것이다.

 

추가로 404에러 코드까지 테스트 코드를 작성 하였다.

 

테스트가 정상적으로 작동이 되며, Movies 배열의 길이 조건과 에러처리 조건이 Pass 된 것을 볼 수 있다.

 

이제 create() 함수에서 에러가 발생하는지의 여부를 테스트 해 보자. delete() 함수를 테스트 했던 코드 아래에 아래와 같이 적어준다.

  describe('create', () => {
    it('should create a movie', () => {
      const beforeCreate = service.getAll().length;
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      const afterCreate = service.getAll().length;
      expect(afterCreate).toBeGreaterThan(beforeCreate);
    });
  });

 

delete() 함수를 테스트 할 때와 마찬가지로 한 개가 추가됐기 때문에,  추가 후의 Movies 배열의 길이가 추가 전의 배열의 길이보다 커야 한다는 것이다.

 

테스트가 정상적으로 작동이 되며, Movies 배열의 길이 조건이 Pass 된 것을 볼 수 있다.

 

참고로, 만약 테스트마다 movie를 생성하기 귀찮다면 beforeEach 테스트 안에서 movie를 아래와 같이 생성해도 된다.

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

    service = module.get<MoviesService>(MoviesService);
    service.create({
      title: 'Test Movie',
      genres: ['test'],
      year: 2000,
    });
  });

 

허나 일단은, 테스트 코드를 직관적으로 확인하기 위해 각각 생성 해 보자.

 

이제 마지막으로 update() 함수에서 에러가 발생하는지의 여부를 테스트 해 보자. create() 함수를 테스트 했던 코드 아래에 아래와 같이 적어준다.

  describe('update', () => {
    it('should update a movie', () => {
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      service.update(1, { title: 'Updated Test' });
      const movie = service.getOne(1);
      expect(movie.title).toEqual('Updated Test');
    });

    it('should throw a NotFoundException', () => {
      try {
        service.update(999, {});
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });

 

movie를 하나 생성 해 주고 생성한 movie를 update 해 준다. update 하는 movie의 id는 1 이고, update 하는 데이터는 title이다. 이후, movie 목록을 확인하고 movie의 title이 "Updated Test"가 맞는지 확인한다.

 

다음으로 NotFoundException 에러를 처리하는지 확인하는 테스트 코드도 같이 넣어주자.

 

테스트가 정상적으로 작동이 되며, Movies 문장 일치 조건과 에러처리 조건이 Pass 된 것을 볼 수 있다.

 

마지막으로 아래 명령어를 입력하여 커버리지를 확인 해 보자.

$ npm run test:cov

 

movies.service.ts 파일이 완벽하게 테스팅 된 것을 확인 할 수 있다!

 

만약 누군가 movies.service.ts 파일에 함수를 하나 더 추가해 놓았으나, spec.ts 파일에 테스트 코드를 입력하지 않으면 바로 확인이 가능하다.

import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';
import { NotFoundException } from '@nestjs/common';

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

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

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

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

  describe('getAll', () => {
    it('should return an array', () => {
    const result = service.getAll()
    expect(result).toBeInstanceOf(Array);
    })
  })

  describe("getOne", () => {
    it("should return a movie", () => {
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      const movie = service.getOne(1);
      expect(movie).toBeDefined();
      expect(movie.id).toEqual(1);
    });

    it("should throw 404 error", () => {
      try{
        service.getOne(999);
      }catch(e){
        expect(e).toBeInstanceOf(NotFoundException);
        expect(e.message).toEqual("Movie with ID 999 not found")
      }
    })
  });

  describe('delete', () => {
    it('deletes a movie', () => {
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      const beforeDelete = service.getAll().length;
      service.delete(1);
      const afterDelete = service.getAll().length;
      expect(afterDelete).toBeLessThan(beforeDelete);
    });
    it('should return a 404', () => {
      try {
        service.delete(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });

  describe('create', () => {
    it('should create a movie', () => {
      const beforeCreate = service.getAll().length;
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      const afterCreate = service.getAll().length;
      expect(afterCreate).toBeGreaterThan(beforeCreate);
    });
  });

  describe('update', () => {
    it('should update a movie', () => {
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      service.update(1, { title: 'Updated Test' });
      const movie = service.getOne(1);
      expect(movie.title).toEqual('Updated Test');
    });

    it('should throw a NotFoundException', () => {
      try {
        service.update(999, {});
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

 

이 spec.ts 파일은 오로지 해당 파일들의 유닛 테스트를 위한 것이다.( e2e 에서는 안 씀 )

 

참조: 노마드코더