icon안동민 개발노트

Jest를 사용한 단위 테스트


 Jest는 JavaScript 프로젝트를 위한 강력한 테스팅 프레임워크로, TypeScript와 함께 사용하면 타입 안전성과 테스트 신뢰성을 높일 수 있습니다.

 이 절에서는 Jest와 TypeScript를 통합하여 효과적인 단위 테스트를 작성하는 방법을 살펴봅니다.

Jest와 TypeScript 설정

  1. 필요한 패키지 설치
npm install --save-dev jest @types/jest ts-jest typescript
  1. Jest 설정 파일 생성
jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
  1. TypeScript 설정
tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "types": ["jest"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

기본 단위 테스트 작성

 함수 테스트 예시

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}
 
// math.test.ts
import { add } from './math';
 
describe('add function', () => {
  it('should add two numbers correctly', () => {
    expect(add(1, 2)).toBe(3);
    expect(add(-1, 1)).toBe(0);
    expect(add(0, 0)).toBe(0);
  });
});

 클래스 테스트 예시

// user.ts
export class User {
  constructor(public name: string, public age: number) {}
 
  isAdult(): boolean {
    return this.age >= 18;
  }
}
 
// user.test.ts
import { User } from './user';
 
describe('User class', () => {
  it('should create a user with name and age', () => {
    const user = new User('John', 25);
    expect(user.name).toBe('John');
    expect(user.age).toBe(25);
  });
 
  it('should correctly determine if a user is an adult', () => {
    const adult = new User('Jane', 20);
    const child = new User('Tom', 15);
    expect(adult.isAdult()).toBe(true);
    expect(child.isAdult()).toBe(false);
  });
});

목(Mock) 사용하기

 TypeScript에서 목 사용 예시

// api.ts
export async function fetchUser(id: number): Promise<User> {
  // 실제 API 호출
}
 
// userService.ts
import { fetchUser } from './api';
 
export async function getUserName(id: number): Promise<string> {
  const user = await fetchUser(id);
  return user.name;
}
 
// userService.test.ts
import { getUserName } from './userService';
import { fetchUser } from './api';
 
jest.mock('./api');
 
describe('getUserName', () => {
  it('should return the user name', async () => {
    const mockFetchUser = fetchUser as jest.MockedFunction<typeof fetchUser>;
    mockFetchUser.mockResolvedValue({ id: 1, name: 'John Doe' });
 
    const name = await getUserName(1);
    expect(name).toBe('John Doe');
    expect(mockFetchUser).toHaveBeenCalledWith(1);
  });
});

비동기 코드 테스트

 Promise와 async/await 테스트

// asyncService.ts
export async function fetchData(): Promise<string> {
  return new Promise(resolve => setTimeout(() => resolve('data'), 1000));
}
 
// asyncService.test.ts
import { fetchData } from './asyncService';
 
describe('fetchData', () => {
  it('should return data after 1 second', async () => {
    const data = await fetchData();
    expect(data).toBe('data');
  });
 
  it('should return data (using done callback)', (done) => {
    fetchData().then(data => {
      expect(data).toBe('data');
      done();
    });
  });
});

스냅샷 테스팅

 TypeScript 컴포넌트의 스냅샷 테스트

// button.tsx
import React from 'react';
 
interface ButtonProps {
  text: string;
  onClick: () => void;
}
 
export const Button: React.FC<ButtonProps> = ({ text, onClick }) => (
  <button onClick={onClick}>{text}</button>
);
 
// button.test.tsx
import React from 'react';
import renderer from 'react-test-renderer';
import { Button } from './button';
 
describe('Button component', () => {
  it('should render correctly', () => {
    const tree = renderer.create(
      <Button text="Click me" onClick={() => {}} />
    ).toJSON();
    expect(tree).toMatchSnapshot();
  });
});

테스트 커버리지

 커버리지 측정 및 보고서 생성

  1. Jest 설정 파일에 커버리지 옵션 추가
module.exports = {
  // ... 기존 설정
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coveragePathIgnorePatterns: ['/node_modules/', '/tests/'],
  coverageReporters: ['text', 'lcov'],
};
  1. 커버리지 리포트 생성
npx jest --coverage

 TypeScript 특유의 커버리지 이슈 해결

  • ts-jestastTransformers 옵션을 사용하여 타입 정보를 제거하고 정확한 커버리지 측정

타입 정보를 활용한 테스트 케이스 생성

 타입 기반 테스트 케이스 생성 예시

type UserRole = 'admin' | 'user' | 'guest';
 
function getUserPermissions(role: UserRole): string[] {
  // 실제 구현
}
 
// 타입을 활용한 테스트 케이스 생성
describe('getUserPermissions', () => {
  const roles: UserRole[] = ['admin', 'user', 'guest'];
 
  roles.forEach(role => {
    it(`should return permissions for ${role}`, () => {
      const permissions = getUserPermissions(role);
      expect(Array.isArray(permissions)).toBe(true);
      // 역할별 특정 검증 로직 추가
    });
  });
});

Jest 설정 최적화

 TypeScript 프로젝트를 위한 Jest 설정 최적화

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.json',
    },
  },
};

Best Practices와 전략

 1. 타입 정의 테스트 : 인터페이스와 타입 정의의 정확성을 검증하는 테스트 작성

 2. 제네릭 활용 : 제네릭 함수와 클래스에 대한 다양한 타입 시나리오 테스트

 3. 타입 가드 테스트 : 사용자 정의 타입 가드 함수의 정확성 검증

 4. 목 타입 안전성 : jest.MockedFunction<T>와 같은 타입을 활용하여 목의 타입 안전성 보장

 5. 비동기 코드 주의사항

  • async/await를 사용하여 가독성 높은 비동기 테스트 작성
  • Promise rejection 테스트 시 expect.assertions()를 사용하여 assertion 횟수 확인

 6. 스냅샷 테스트 전략

  • UI 컴포넌트의 렌더링 결과를 스냅샷으로 저장
  • 스냅샷 업데이트 시 신중하게 검토

 7. 테스트 구조화

  • describeit을 사용하여 테스트를 논리적으로 그룹화
  • beforeEachafterEach를 활용하여 테스트 환경 설정 및 정리

 8. 커버리지 목표 설정

  • 프로젝트의 중요도에 따라 적절한 커버리지 목표 설정 (예 : 80% 이상)
  • 핵심 비즈니스 로직에 대해서는 높은 커버리지 유지

 9. 지속적 통합(CI) 파이프라인 구축

  • 커밋마다 자동으로 테스트 실행 및 커버리지 리포트 생성
  • 커버리지 임계값을 설정하여 품질 관리

 10. 문서화

  • 각 테스트 케이스의 목적과 테스트하는 시나리오를 명확히 설명
  • 복잡한 설정이 필요한 테스트의 경우 주석으로 상세 설명 추가