TypeScript 단위 테스트 코드 작성하기 (1) - 단위 테스트

Jiseong Choi·2025년 4월 7일

TypeScript

목록 보기
2/2

이번 포스트에서는 TypeScript 단위테스트에서 작성해보려고한다!
지금까지 개발을 할 때 코드를 작성하고 Postman으로 직접 테스트하면서 오류를 찾았었다.
테스트 코드의 중요성을 못느끼고 살았는데 TypeScript를 공부하면서 테스트 코드 작성도 같이 공부해보려고 한다!
테스트 코드를 작성하면서 느낀점은 기존에 작성했던 코드를 수정할 일이 생겼을 때 직접 API를 호출해서 테스트를 안하고 테스트 코드를 실행하기만 하면 테스트가 된다는 점에서 테스트 코드 작성의 중요성을 느꼈다!
테스트 코드의 종류로는 단위 테스트, 통합 테스트 등 여러가지가 있는데 나는 일단 단위 테스트 먼저 정리해보려고 한다.
나중에 통합 테스트(Integration Test), 부하 테스트 / 스트레스 테스트(Load / Stress Test) 이렇게 정리해보려고 한다!


단위 테스트(Unit Test)란?

단위 테스트는 프로그램의 가장 작은 단위인 함수나 메서드가 예상대로 동작하는지 검증하는 테스트다

쉽게 말하면, "이 함수가 입력을 받았을 때, 기대한 결과를 내보내는가?"를 자동으로 체크하는 코드!

예시

// 함수
function add(a: number, b: number): number {
	return a + b;
}

// 단위 테스트 (Jest)
test('add 함수는 두 수의 합을 반환해야 한다.', () => {
	expect(add(2, 5)).toBe(5);
});

단위 테스트를 작성하는 이유는?

1. 코드의 신뢰성 확보

  • 기능이 바뀌어도 기존 함수들이 잘 작동하는지 자동으로 검증할 수 있다.

2. 리팩토링할 때 안심

  • 코드 구조를 바꾸더라도 "기능은 그대로"라는 걸 테스트로 증명할 수 있다.

3. 빠른 피드백

  • 개발 중 바로바로 오류를 파악할 수 있어 디버깅 시간이 줄어든다.

단위 테스트의 특징

  • 테스트 범위: 아주 작다(함수, 메서드 단위)
  • 속도: 매우 빠르다.
  • 외부 의존성: 거의 없다 (DB, 네트워크, 파일시스템과 분리)
  • 테스트 도구: Jest, Mocha, JUnit, Pytest 등
  • 테스트 대상: 순수 함수, 클래스 메서드 등

단위 테스트의 한계

  • 전체 흐름(비즈니스 로직)을 테스트하기 어렵다.
  • 실제 환경(DB, API 등)과 차이가 있어 테스트 결과가 실제와 다를 수 있다.
  • 잘못된 mock 설정은 잘못된 신뢰를 줄 수 있다.

단위 테스트를 잘 작성하는 방법

  1. AAA 패턴 사용한다.
    • Arrange(준비), Act(실행), Assert(검증) 구조로 테스트를 작성한다.
  2. 테스트는 독립적으로 한다.
    • 하나의 테스트가 다른 테스트에 영향을 주지 않도록 작성한다.
  3. 의미 있는 테스트 이름을 작성한다.
    • 어떤 기능을 테스트하는지 명확하게 표현한다.
  4. mock/stub를 활용한다.
    • 외부 의존성(DB, API 등)은 mocking/stubbing으로 제거한다.
  5. 하나의 테스트는 하나의 검증만한다.
    • 실패 시 원인을 명확하게 알 수 있다.
  6. 테스트 커버리지보다 품질 우선.
    • 모든 함수, 메서드를 테스트 하는것 보다, 중요한 로직 테스트를 우선으로 테스트 코드를 작성한다.

단위 테스트 환경을 설정해보자!

Jest 테스트 프레임워크를 활용한 테스트 환경을 설정할 것이다!

1️⃣ 첫번째로 테스트 프레임워크를 설치한다.

npm install --save-dev jest ts-jest @types/jest

2️⃣ 두번째로 설정 파일을 초기화한다.

// jest.config.js 생성
npx ts-jest config:init

나의 jest.config.js의 초기 설정은 이렇다!

/** @type {import('ts-jest').JestConfigWithTsJest} **/
// ts-jest를 사용하는 Jest 설정임을 명시하는 타입 선언 (자동 완성 및 타입 체크 지원)

module.exports = {
	preset: 'ts-jest',
    testEnvironment: 'node',	// 테스트 실행 환경을 node로 설정
    roots: ['<rootDir>/src'],	// root directory 설정
    testMatch: [	// 테스트 파일 경로 패턴 지정
    	'**/tests/**/*.test.ts'
    ],
    transform: {	// .ts 또는 .tsx 파일을 ts-jest를 이용해 반환
    	"^.+\.tsx?$": ['ts-jest', {}],
    },
    
    // 테스트 커버리지 리포트를 생성
    collectCoverage: true,
    converageDirectory: 'coverage/',
    
    // Jest가 모듈을 찾을 때 참조할 디렉토리들
    moduleDirectories: ['node_modules', 'src'],
    moduleNameMapper: {
    	"^@/(.*)$": '<rootDir>/src/$1',
        "^@tests/(.*)$": '<rootDir>/tests/$1',
        ...
    }
};

초기 설정 속성

옵션설명
testEnvironment테스트가 실행될 환경. 보통 "node" 또는 "jsdom" 사용.
transform파일을 테스트 전에 어떻게 변환할지 설정. TypeScript라면 ts-jest 사용.
testMatch어떤 파일을 테스트로 인식할지 glob 패턴으로 지정.
testRegextestMatch 대신 정규식으로 테스트 파일 경로 지정 가능.
collectCoveragetrue로 설정하면 테스트 커버리지 리포트를 생성함.
coverageDirectory커버리지 리포트를 저장할 디렉토리 이름 설정. 기본값은 "coverage".
coveragePathIgnorePatterns커버리지 측정에서 제외할 경로 목록.
moduleNameMapper경로 별칭(alias) 설정할 때 사용. 예: @/src/
setupFiles테스트 실행 전에 설정할 스크립트 목록. 예: 환경 변수 설정
setupFilesAfterEnv각 테스트 환경 설정 후 실행할 스크립트 (예: jest-extended, @testing-library/jest-dom)
moduleFileExtensionsJest가 인식할 파일 확장자 목록. 기본값: ["js", "json", "jsx", "ts", "tsx", "node"]
rootsJest가 테스트를 찾을 디렉토리 목록. 기본값은 ["<rootDir>"].
globals전역 설정값 지정. 예: ts-jest 관련 설정
verbosetrue로 설정하면 테스트 실행 결과를 더 자세히 출력함.
bail실패한 테스트가 있으면 즉시 중단할지 여부. 기본은 false.

jest.config.js 파일 초기 설정을 할 때 조금 애먹었던건 tsConfig.js 설정에서 paths로 설정해뒀던 경로들을 jest.config.js에도 설정을 해야한다는 거였다!
설정을 안하면 별칭으로 import하는 클래스들을 찾아오지 못한다..ㅎ

3️⃣ 세번째로 단위 테스트용 디렉토리를 분리한다.

보통 단위 테스트용 데이터를 모아두는 디렉토리로 __mock__/, factory/, fixtures/를 사용한다고 한다.

폴더 구조 예시를 보여주겠다!

project-root/
├── src/
│
├── tests/                            ← 테스트 전용 디렉토리
│   ├── unit/                         ← 단위 테스트
│   │   ├── utils/
│   │   │   └── utils.test.ts
│   │   └── services/
│   │       └── post.service.test.ts
│   │
│   ├── integration/                  ← 통합 테스트
│   │   └── user-flow.test.ts
│   │
│   ├── __mocks__/                    ← 모킹 객체/모듈
│   │   ├── axios.ts                  ← 예: axios 모킹
│   │   └── fs.ts
│   │
│   ├── factory/                      ← 테스트용 객체 생성 함수
│   │   └── userFactory.ts
│   │
│   ├── fixtures/                     ← 고정된 더미 데이터 (JSON 등)
│   │   ├── sample-user.json
│   │   └── config-fixture.ts
│   │
│   ├── setupTests.ts                ← 테스트 환경 초기 설정 (예: jest setup)
│   └── jest.global.d.ts             ← 전역 타입 정의 (필요시)
│
├── jest.config.ts
├── tsconfig.json
└── package.json

📁 디렉터리 설명

  • unit - 컴포넌트나 서비스 등 모듈 단위 테스트
  • intergration - 여러 모듈이 엮인 흐름 테스트
  • __mocks__ - 모듈이나 라이브러리를 mocking할 때 사용 (axios, fs, i18n 등)
  • factory - 객체 생성 도우미 함수들 (동적 더미 객체 생성)
  • fixtures - 고정된 더미 데이터 (예: JSON, config 등)
  • setupTests.ts - 글로벌 테스트 설정 (ex: jest.useFakeTimers(), cleanup() )
  • jest.global.d.ts - Jest 커스텀 matchers, 글로벌 변수 타입 정의

4️⃣ 네번째로 테스트용 코드를 작성한다.

실제 데이터베이스에 접근하는 PostDao와 같은 DB 접근 객체는 jest.fn()을 사용해 mock으로 대체하여 테스트 한다.
jest.fn()은 함수를 mocking, jest.mock()은 모듈 자체를 mocking 하는 것이다.
describe()는 관련 테스트들을 그룹화하고, it() 또는 test()는 각각의 테스트 케이스를 작성하는 데 사용한다.

/**
 * Post Service Unit Test 파일
 */

import 'reflect-metadata';
import { PostService } from '@/services/post.service';
import { S3FileStorageService } from "@/services/file.service";
import { PostDao } from "@/daos/post.dao";
import { describe } from "node:test";

// jest.mock: 특정 모듈 전체를 mock 처리할 때 사용
jest.mock('@/services/file.service');

describe('PostService', () => {
  let postService: PostService;
  let s3FileStorageService: jest.Mocked<S3FileStorageService>;
  let postDao: jest.Mocked<PostDao>;

  beforeEach(() => {
    // S3FileStorageService를 mock instance로 생성
    s3FileStorageService = {
      deleteFiles: jest.fn(),
    } as unknown as jest.Mocked<S3FileStorageService>;

	// postDao mock 생성
    postDao = {
      findAll: jest.fn(),	// 함수 mocking
      findById: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
    } as unknown as jest.Mocked<PostDao>;

    postService = new PostService(postDao, s3FileStorageService);
  });

  // describe: findAll 테스트 그룹화 
  describe('findAll()', () => {
    // it: 테스트 케이스 작성(test로 작성해도 무방)
    it("전체 게시글 조회에 성공하고 데이터를 반환한다.", async () => {
      const mockPosts = [{id: 1, title: '테스트 게시글'}];
     
      // postDao.findAll의 응답을 정한다.
      postDao.findAll.mockResolvedValue({
        success: true,
        data: mockPosts
      });

	  // postService.findAll() 실행
      const result = await postService.findAll();

	  // 예상 응답과, 실제 응답이 같은지 비교한다.
      expect(result).toEqual({ success: true, data: mockPosts });
      expect(postDao.findAll).toHaveBeenCalled();
    });

    it("전체 게시글 조회 내역이 없어서 404 오류를 반환한다.", async () => {
      postDao.findAll.mockResolvedValue({
        success: false,
        data: []
      });

      await expect(postService.findAll()).rejects.toThrow('게시글을 찾을 수 없습니다.');
      expect(postDao.findAll).toHaveBeenCalled();
    })

    it("전체 게시글 조회 중 오류 발생으로 500 에러 반환", async () => {
      postDao.findAll.mockResolvedValue({
        success: false,
        error: "전체 Post 조회 중 문제 발생"
      });

      await expect(postService.findAll()).rejects.toThrow('전체 Post 조회 중 문제 발생');
      expect(postDao.findAll).toHaveBeenCalled();
    })
  });
});

5️⃣ 다섯번째로 테스트 실행 명령어를 입력하여 테스트를 한다.

npx jest를 직접 console에 입력해서 실행을 해도 되고, package.json scripts에 등록해서 사용해도 된다!

Scripts 추가 예시

npx jest 관련 옵션

  • --watchAll :
    프로젝트의 모든 테스트를 감시하면서 실행한다.
    (git 상태 무시하고 전체 실행)
  • --watch :
    Git의 변경된 파일들만 감시해서 테스트 실행
    (예: git status로 modified만 파일들 중심)
  • --coverage :
    테스트 커버리지 리포트도 함께 출력
profile
나 혼자 공부하고, 끄적이는 공간. (Node.JS / Back-End Developer)

0개의 댓글