목록으로

Jest를 활용한 함수 테스트 (with NestJS)

약 16분 읽기Development#Jest#NestJS댓글로 이동

함수 테스트

main
date
Jun 1, 2024
slug
jest-with-nestjs
author
status
Public
tags
Jest
NestJS
summary
함수 테스트
type
Post
thumbnail
category
Development
updatedAt
Oct 12, 2025 10:43 AM
Jest를 활용하여 함수를 작성하면 스크립트 실행만으로 내가 작성한 함수가 올바르게 동작하는지 확인을 할 수가 있다.
NestJS CLI 를 통해서 프로젝트를 셋업하면 기본적으로 Jest가 설치된다.
notion image
 
// 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" } }
package.json을 살펴보면 .spec.ts 라는 이름이 붙은 파일의 경우 테스트 파일로 읽도록 셋업 되어있는 것을 확인할 수 있다.
notion image
 
{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json" } }
백엔드에서 내가 만든 함수가 제대로 동작하는지 확인을 하고 싶을 때 포스트맨으로 해당 api에 요청을 보내 테스트를 했는데 Jest를 이용하면 이러한 과정을 생략하고 원하는 함수만 테스트를 해볼 수 있다.

1. 모듈을 사용하지 않은 함수 테스트 하기

Nest 모듈을 사용하지 않는 다는 건 일반 함수를 분리하여 이를 Nest 모듈 내에 사용하는 방식으로 외부 함수를 모듈 내에서 불러와 사용한다.
 
notion image
간단한 예시로 유니서베이 작업에서 사용했던 테스트 케이스이다.
문항의 순서가 변경됐을 때 쿼터 문항을 RECODE 문항 뒤 쪽으로 위치하도록 하는 함수를 작성할 것이다.

테스트 시나리오

describe("유니서베이 문항 순서 변환 테스트", () => { test("쿼터 문항의 위치를 SQ_RECODE 뒤 쪽에 위치하도록 한다.", () => {}); });

샘플 데이터

const data = [ { questionIndex: 1, questionNumber: "DESC", }, { questionIndex: 2, questionNumber: "SQ1", }, { questionIndex: 3, questionNumber: "SQ2", }, { questionIndex: 4, questionNumber: "SQ2_RECODE", }, { questionIndex: 5, questionNumber: "SQ3", }, { questionIndex: 6, questionNumber: "QUOTA_SQ1_SQ2", }, ];

원하는 형태의 결과 데이터

const expectedData = [ { questionIndex: 1, questionNumber: "DESC", }, { questionIndex: 2, questionNumber: "SQ1", }, { questionIndex: 3, questionNumber: "SQ2", }, { questionIndex: 4, questionNumber: "SQ2_RECODE", }, { questionIndex: 5, questionNumber: "QUOTA_SQ1_SQ2", }, { questionIndex: 6, questionNumber: "SQ3", }, ];
위와 같은 형태의 데이터로 반환하도록 하는 함수를 작성한다.
 
type InsertQuestionDto = { questionIndex: number; questionNumber: string; }; /**@description 쿼터 모듈 추가 - SQ2 문항 뒤로 순서 바꾸기 * @param {InsertQuestionDto[]} question 설문 정보가 담긴 배열 data * @param {string} cursor 이동할 문항의 인덱스가 될 questionNumber * @param {string} target 이동시킬 문항 questionNumber * */ export const changeQuestionsIndex = ( question: InsertQuestionDto[], cursor: string, target: string, ): InsertQuestionDto[] => { return []; }; describe("유니서베이 문항 순서 변환 테스트", () => { // jest Mock을 사용하여 가상의 목킹 함수를 생성 let changeQuestionsIndexMockFn: jest.Mock<InsertQuestionDto[], [InsertQuestionDto[], string, string]>; // 실제 데이터가 들어갈 곳 let data: InsertQuestionDto[] = []; // 내가 원하는 데이터 형태 let expectedData: InsertQuestionDto[] = []; // 필요한 함수 및 데이터 셋업 beforeEach(() => { // 함수 초기화 changeQuestionsIndexMockFn = jest.fn(changeQuestionsIndex); // 데이터 셋업 data = [ { questionIndex: 1, questionNumber: "DESC", }, { questionIndex: 2, questionNumber: "SQ1", }, { questionIndex: 3, questionNumber: "SQ2", }, { questionIndex: 4, questionNumber: "SQ2_RECODE", }, { questionIndex: 5, questionNumber: "SQ3", }, { questionIndex: 6, questionNumber: "QUOTA_SQ1_SQ2", }, ]; // 내가 원하는 데이터 형태 expectedData = [ { questionIndex: 1, questionNumber: "DESC", }, { questionIndex: 2, questionNumber: "SQ1", }, { questionIndex: 3, questionNumber: "SQ2", }, { questionIndex: 4, questionNumber: "SQ2_RECODE2", }, { questionIndex: 5, questionNumber: "QUOTA_SQ1_SQ2", // <-- 변경 }, { questionIndex: 6, questionNumber: "SQ3", // <-- 변경 }, ]; }); test("쿼터 문항의 위치를 SQ_RECODE 뒤 쪽에 위치하도록 한다. target을 cursor 위치로 이동", () => { // target을 cursor 위치로 이동 const target = "QUOTA_SQ1_SQ2"; const cursor = "SQ3"; expect(changeQuestionsIndexMockFn(data, cursor, target)).toEqual(expectedData); }); });
 
notion image
테스트를 실행하면 당연히 실패하는 함수가 된다. (현재 함수 내에 아무런 로직을 작성하지 않았기 때문 빈배열 반환)

함수 작성

/**@description 쿼터 모듈 추가 - SQ2 문항 뒤로 순서 바꾸기 * @param {InsertQuestionDto[]} question 설문 정보가 담긴 배열 data * @param {string} cursor 이동할 문항의 인덱스가 될 questionNumber * @param {string} target 이동시킬 문항 questionNumber * */ export const changeQuestionsIndex = ( question: InsertQuestionDto[], cursor: string, target: string, ): InsertQuestionDto[] => { const cursorIndex = question.findIndex((v) => v.questionNumber === cursor); const targetIndex = question.findIndex((v) => v.questionNumber === target); function moveValue( array: InsertQuestionDto[], fromIndex: number, toIndex: number, ) { const item = array.splice(fromIndex, 1)[0]; array.splice(toIndex, 0, item); } moveValue(question, targetIndex, cursorIndex); return question .map((value, index) => { return { ...value, questionIndex: index + 1, }; }) .sort((a, b) => a.questionIndex - b.questionIndex); };
함수 작성이 끝났으면 다시 실패했던 테스트 케이스를 실행 시켜보면 실패했던 케이스가 성공하는 케이스로 반환된다.
notion image
그렇다면 반환 데이터를 임의로 변경해보면?? (잘못된 값으로 반환될 수도 있으니까 😮)
 
expectedData = [ . . { questionIndex: 4, questionNumber: "SQ2_RECODE2", // data는 SQ2_RECODE 인데 exptededData 는 SQ2_RECODE2 }, //.. 이하 생략 ];
notion image
변경한 부분만 잘못됐다고 실패하는 테스트 케이스가 된다.
이렇게 함수를 테스트할 때 내가 원하는 데이터가 어떤 형태인지 정하고, 실제로 들어가는 데이터는 어떤 형태인지 정한 후에 내가 원하는 데이터가 나오도록 함수를 작성하면 잘못된 곳이 어딘지 확인할 수 있다.
만약 테스트 케이스가 여러 개라면 마찬가지로 각각의 데이터 형태를 정의하고 여러 테스트 케이스를 작성해주면 된다.
 
describe("process.env.COLLECTOR_PROJECT_NUMBER CHECK", () => { let collectorSurveyCheckMockFn: jest.Mock<boolean, [number]>; let projectNumber = 0; beforeEach(() => { collectorSurveyCheckMockFn = jest.fn(collectorSurveyCheck); projectNumber = process.env.COLLECTOR_PROJECT_NUMBER; }); test.skip("process.env.COLLECTOR_PROJECT_NUMBER을 읽을 수 있는지 체크한다.", () => { const expectedNumber = 10000000; expect(projectNumber).not.toEqual(expectedNumber); }); test.skip("process.env.COLLECTOR_PROJECT_NUMBER가 숫자형인지 체크한다.", () => { const expectedNumber = 10000000; expect(typeof projectNumber).toEqual(typeof expectedNumber); }); test.skip("process.env.COLLECTOR_PROJECT_NUMBER에 문자열이 들어갔을 때 NaN을 반환하는지 체크한다.", () => { expect(String(projectNumber)).toBeNaN(); }); test.skip("SNUM을 넘겼을 때 boolean 값을 넘겨 받는지 체크한다.", () => { const SNUM = 9999999; expect(collectorSurveyCheckMockFn(SNUM)).toBeTruthy(); }); test.skip("process.env.COLLECTOR_PROJECT_NUMBER 가 NaN 일 때 DEFAULT_NUMBER를 반환하는지 확인", () => { const SNUM = 9999999; expect(collectorSurveyCheckMockFn(SNUM)).toEqual(DEFAULT_NUMBER); }); test.skip("최종 함수 테스트 콜렉터는 true, 메타서베이는 false를 반환해야한다.", () => { const SNUM = 1000001; // 유니 설문 expect(collectorSurveyCheckMockFn(SNUM)).toBeTruthy(); }); });
 

Nest 모듈을 이용하여 Service 함수 테스트 하기

일반 함수 테스트의 경우 export 된 함수를 .spec.ts 파일에 import 해서 함수를 테스트 했지만 Nest Service를 테스트 하기 위해서는 가상의 Nest Module을 만들어서 사용 해야한다.
Nest Cli 로 생성했을 때 생성된 app.controller.spec.ts를 보면 가상의 테스팅 모듈을 만들어서 사용한 것을 확인할 수 있다.
 
// app.controller.spec.ts 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!'); }); }); });
 
클라이언트에서 전달 받은 값에 따라 지역 정보 배열을 반환하는 서비스 함수를 작성해보자.
 
const locationValue1 = ['전국']; // 전국 const locationValue2 = ['서울', '인천/경기']; // 서울 / 인천 / 경기 const locationValue3 = ['서울', '지방 5대 광역시']; // 서울 / 5대광역시 const locationValue4 = ['인천/경기', '지방 5대 광역시']; // 인천 / 경기 / 5대광역시 const locationValue5 = ['서울']; // 서울 / 인천 / 경기 const locationValue6 = ['인천/경기']; // 서울 / 5대광역시 const locationValue7 = ['지방 5대 광역시']; // 인천 / 경기 / 5대 describe('전달 받은 값에 따라 지역 정보 배열을 반환합니다.', () => { let quotaCalculationService: QuotaCalculationService; let getRegionDataMockFn: jest.Mock<string[], [string[]]>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ QuotaCalculationService, { provide: I18nService, useValue: {} }, { provide: TargetQuotaRepository, useValue: {} }, ], }).compile(); quotaCalculationService = module.get<QuotaCalculationService>( QuotaCalculationService, ); getRegionDataMockFn = jest.fn(quotaCalculationService.getRegionData); }); test('전국을 입력했을 경우 지역 전체 배열을 반환 - CASE 1', () => { expect(getRegionDataMockFn(locationValue1)).toEqual(type1); }); test('서울을 입력했을 경우 서울을 반환 - CASE 2', () => { expect(getRegionDataMockFn(locationValue5)).toEqual(type2); }); test('인천 / 경기를 입력했을 경우 인천 경기 세종을 반환 - CASE 3', () => { expect(getRegionDataMockFn(locationValue6)).toEqual(type3); }); test('지방 5대 광역시를 입력했을 경우 CASE 2 3을 제외한 지역 반환 - CASE4 ', () => { expect(getRegionDataMockFn(locationValue7)).toEqual(type4); }); test('서울 / 인천 / 경기 입력했을 경우 - CASE 5', () => { expect(getRegionDataMockFn(locationValue2)).toEqual(type5); }); test('인천 / 경기 / 지방5대 광역시를 입력했을 경우 인천 경기 세종을 반환 - CASE 6', () => { expect(getRegionDataMockFn(locationValue4)).toEqual(type7); }); test('서울 / 지방 5대 광역시를 입력했을 경우 CASE 2 3을 제외한 지역 반환 - CASE 6', () => { expect(getRegionDataMockFn(locationValue3)).toEqual(type6); }); });
 
사용할 서비스는 QuotaCalculationService에 있는 getRegionData 함수를 테스를 하기 위해 가상의 테스팅 모듈을 생성
이번 함수에서는 I18n과 TargetQuotaRepository는 사용하지 않지만 의존 주입을 위해 Testing Module의 Provider에 등록

Service

@Injectable() export class QuotaCalculationService { constructor( private readonly i18n: I18nService, private readonly targetQuotaRepository: TargetQuotaRepository, ) {} // 지역 정보 반환 getRegionData(locations: string[]): string[] { return []; } }
만약 [‘전국’] 이라는 값이 getRegionData에 전달된다면 getRegionData는 [‘서울’,’부산’,’대구’,’인천’,’광주’,’대전’,’울산’,’세종’,’경기’,’강원’,’충북’,’충남’,’전북’,’전남’,’경북’,’경남’,’제주’] 를 반환해야하고,
[‘서울’, ‘인천/경기’] 라는 값이 전달되면 [‘서울’,’인천’,’경기’,’세종’] 을 반환하는 함수를 작성해야한다.
notion image
@Injectable() export class QuotaCalculationService { constructor( private readonly i18n: I18nService, private readonly targetQuotaRepository: TargetQuotaRepository, ) {} // 지역 정보 반환 private getRegionData(locations: string[]): string[] { return locations .map((location) => { return regionRepository[location]; }) .filter((v) => v) .flat(2); } }
notion image
이렇게 각 케이스 별로 반환 값을 확인할 수 있기 때문에 혹시나 놓친 케이스가 있는지 확인하기도 쉽고 번거롭게 해당 함수를 사용하는 컨트롤러에 api를 요청하지 않고도 함수를 확인할 수 있다.
위에는 단순히 서비스 내에서 작성된 함수를 테스트 했는데 DB를 조회해서 테스트를 해야한다면 어떻게 해야나

Service

// projectService @Injectable() export class ProjectService { constructor( private readonly projectRepository: ProjectRepository, ) {} async findOneBySurveyId(surveyId: string) { return this.projectRepository.findOne(surveyId); // 서비스 함수 내에서 레포지토리 호출 } }

Repository

@Injectable() export class ProjectRepository { constructor( @InjectModel(Project.name, ConnectionNames.PANEL_HUB_V3) private readonly projectModel: Model<ProjectDocument>, ) {} async findOne(surveyId: string): Promise<ProjectDto> { return this.projectModel.findOne({ surveyId }); } }
findOne은 surveyId를 전달받아 해당 하는 프로젝트 정보를 반환해주는 함수이다.

가상의 프로젝트 정보

export const project = { "_id" : new Types.ObjectId("6604cf7d5d4537c754a6b733"), "managers" : [ { "userId" : "dhjeon@pmirnc.com", "name" : "다훈" } ], "title" : "asdasads", "regionType" : "국내", "comments" : { "rejectComment" : "", "estimateComment" : "" }, "suppliers" : [ "GS", "LIME", "WIZ_PANEL" ], "deviceTypes" : [ "테블릿" ], "profileSurvey" : false, "profileFilter" : {}, "introHtml" : "", "outroHtml" : "", "exceptSurveyIds" : [], "includeSurveyIds" : [], "createDate" : new Date("2024-03-28T11:00:48.229+09:00"), "startDate" : new Date("2024-03-28T11:01:00.000+09:00"), "endDate" : new Date("2024-04-27T23:59:59.000+09:00"), "status" : "진행", "lang" : "en", "addRespondentInfo" : "", "partnerName" : "", "partnerId" : "", "partnerInfo" : null, "panelReady" : false, "overseas" : false, "joinUrl" : "https://naver.com/[UID]", "testUrl" : "https://naver.com/[UID]", "quotaUrl" : "", "payment" : { "status" : true, "date" : new Date("2024-04-01T09:39:07.703+09:00"), "cost" : 2444, "method" : "card", "orderId" : "결제 완료됐을 경우 부여받은 ID 기입" }, "quotaStatus" : "균등", "preliminaryQuestion" : true, "reward" : { "GS" : { "qualified" : 110, "terminate" : 50, "quotaFull" : 50, "_id" : new Types.ObjectId("6604cf935d4537c754a6b73f") }, "LIME" : { "qualified" : 100, "terminate" : 20, "quotaFull" : 20, "_id" : new Types.ObjectId("6604cf935d4537c754a6b740") }, "WIZ_PANEL" : { "qualified" : 90, "terminate" : 30, "quotaFull" : 30, "_id" : new Types.ObjectId("6604cf935d4537c754a6b741") } }, "surveyCategory" : { "category" : "생활용품", "etcValue" : "" }, "surveyId" : "82a52d1f-66ae-4e42-aac5-1489fb99d3fd", "projectId" : 300181, "creatorId" : "dhjeon@pmirnc.com", "updateId" : "dhjeon@pmirnc.com", "groupId" : "PMI", "createdAt" : new Date("2024-03-28T11:01:33.199+09:00"), "updatedAt" : new Date("2024-04-02T16:23:44.908+09:00"), "__v" : 0, "demoGraphic" : { "age" : [], "region" : [], "gender" : [ "남성", "여성" ], "_id" : new Types.ObjectId("6604cf935d4537c754a6b73e") }, "estimatedCount" : 2222, "estimatedLOI" : 2 }
실제 DB를 조회하는게 아니고 DB에서 조회했을 때 반환되는 결과값을 임의로 정의해두고 이를 활용해 테스트 함수를 작성하면 된다.
describe("프로젝트 서비스 테스트", () => { let projectService: ProjectService; let projectRepository: ProjectRepository; let projectModel: Model<ProjectDocument>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [], providers: [ ProjectService, ProjectRepository, { provide: getModelToken(Project.name, ConnectionNames.PANEL_HUB_V3), useValue: {}, } ] }).compile(); projectService = module.get<ProjectService>(ProjectService); projectRepository = module.get<ProjectRepository>(ProjectRepository); projectModel = module.get<Model<ProjectDocument>>(getModelToken(Project.name, ConnectionNames.PANEL_HUB_V3)) }) test('surveyId를 통해 프로젝트 하나를 조회한다.', async () => { const surveyId = '82a52d1f-66ae-4e42-aac5-1489fb99d3fd'; jest.spyOn(projectRepository, 'findOne') .mockImplementationOnce(() => Promise.resolve(project)); // findOne 함수가 가상으로 반환하는 값을 넣어준다. const findOneBySurveyId = await projectService.findOneBySurveyId(surveyId) expect(findOneBySurveyId).toStrictEqual(project); // 반환할 때 예상하는 값을 넣어준다. }) })
.mockImplementationOnce(() => Promise.resolve(project)) 통해서 반환하는 값을 위에서 정의한 project를 주었고 findOneBySurveyId의 예상 값도 project를 주었기 때문에 해당 케이스는 실패하지 않는 케이스이다.
만약 findOne을 호출한 후 findOneBySurveyId 에서 가공하여 다른 값을 반환한다면 실패하는 케이스가 되어 .toStrictEqual(project) 여기에 가공한 데이터를 전달해주면 성공하는 케이스로 만들 수 있다.