한국어

고급 Jest 테스트 패턴을 마스터하여 더 안정적이고 유지보수하기 쉬운 소프트웨어를 구축하세요. 전 세계 개발팀을 위해 모킹, 스냅샷 테스트, 커스텀 매처 등의 기술을 탐색해 보세요.

Jest: 견고한 소프트웨어를 위한 고급 테스트 패턴

오늘날 빠르게 변화하는 소프트웨어 개발 환경에서 코드베이스의 신뢰성과 안정성을 보장하는 것은 무엇보다 중요합니다. Jest는 자바스크립트 테스트의 사실상 표준이 되었지만, 기본적인 단위 테스트를 넘어 애플리케이션에 대한 새로운 차원의 확신을 얻을 수 있습니다. 이 글에서는 전 세계 개발자들을 대상으로 견고한 소프트웨어를 구축하는 데 필수적인 고급 Jest 테스트 패턴에 대해 자세히 알아봅니다.

기본 단위 테스트를 넘어서야 하는 이유

기본 단위 테스트는 개별 구성 요소를 격리하여 검증합니다. 그러나 실제 애플리케이션은 구성 요소가 상호 작용하는 복잡한 시스템입니다. 고급 테스트 패턴은 다음과 같은 작업을 가능하게 하여 이러한 복잡성을 해결합니다:

모킹(Mocking)과 스파이(Spies) 마스터하기

모킹은 테스트 대상 단위를 의존성으로부터 격리하기 위해 제어된 대체물로 교체하는 데 매우 중요합니다. Jest는 이를 위한 강력한 도구를 제공합니다:

jest.fn(): 모크와 스파이의 기초

jest.fn()은 모의 함수(mock function)를 생성합니다. 호출, 인수, 반환 값을 추적할 수 있습니다. 이는 더 정교한 모킹 전략을 위한 기본 구성 요소입니다.

예시: 함수 호출 추적하기

// component.js
export const fetchData = () => {
  // API 호출 시뮬레이션
  return Promise.resolve({ data: 'some data' });
};

export const processData = async (fetcher) => {
  const result = await fetcher();
  return `Processed: ${result.data}`;
};

// component.test.js
import { processData } from './component';

test('should process data correctly', async () => {
  const mockFetcher = jest.fn().mockResolvedValue({ data: 'mocked data' });
  const result = await processData(mockFetcher);
  expect(result).toBe('Processed: mocked data');
  expect(mockFetcher).toHaveBeenCalledTimes(1);
  expect(mockFetcher).toHaveBeenCalledWith();
});

jest.spyOn(): 교체 없이 관찰하기

jest.spyOn()을 사용하면 기존 객체의 메서드 구현을 반드시 교체하지 않고도 해당 메서드에 대한 호출을 관찰할 수 있습니다. 필요한 경우 구현을 모킹할 수도 있습니다.

예시: 모듈 메서드 스파이하기

// logger.js
export const logInfo = (message) => {
  console.log(`INFO: ${message}`);
};

// service.js
import { logInfo } from './logger';

export const performTask = (taskName) => {
  logInfo(`Starting task: ${taskName}`);
  // ... 작업 로직 ...
  logInfo(`Task ${taskName} completed.`);
};

// service.test.js
import { performTask } from './service';
import * as logger from './logger';

test('should log task start and completion', () => {
  const logSpy = jest.spyOn(logger, 'logInfo');

  performTask('backup');

  expect(logSpy).toHaveBeenCalledTimes(2);
  expect(logSpy).toHaveBeenCalledWith('Starting task: backup');
  expect(logSpy).toHaveBeenCalledWith('Task backup completed.');

  logSpy.mockRestore(); // 원본 구현으로 복원하는 것이 중요합니다
});

모듈 임포트 모킹하기

Jest의 모듈 모킹 기능은 광범위합니다. 전체 모듈 또는 특정 export를 모킹할 수 있습니다.

예시: 외부 API 클라이언트 모킹하기

// api.js
import axios from 'axios';

export const getUser = async (userId) => {
  const response = await axios.get(`/api/users/${userId}`);
  return response.data;
};

// user-service.js
import { getUser } from './api';

export const getUserFullName = async (userId) => {
  const user = await getUser(userId);
  return `${user.firstName} ${user.lastName}`;
};

// user-service.test.js
import { getUserFullName } from './user-service';
import * as api from './api';

// 전체 api 모듈을 모킹합니다
jest.mock('./api');

test('should get full name using mocked API', async () => {
  // 모킹된 모듈에서 특정 함수를 모킹합니다
  api.getUser.mockResolvedValue({ id: 1, firstName: 'Ada', lastName: 'Lovelace' });

  const fullName = await getUserFullName(1);

  expect(fullName).toBe('Ada Lovelace');
  expect(api.getUser).toHaveBeenCalledTimes(1);
  expect(api.getUser).toHaveBeenCalledWith(1);
});

자동 모킹 vs. 수동 모킹

Jest는 Node.js 모듈을 자동으로 모킹합니다. ES 모듈이나 커스텀 모듈의 경우 jest.mock()이 필요할 수 있습니다. 더 많은 제어를 위해 __mocks__ 디렉토리를 생성할 수 있습니다.

모의 구현 (Mock Implementations)

모크에 대한 커스텀 구현을 제공할 수 있습니다.

예시: 커스텀 구현으로 모킹하기

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// calculator.js
import { add, subtract } from './math';

export const calculate = (operation, a, b) => {
  if (operation === 'add') {
    return add(a, b);
  } else if (operation === 'subtract') {
    return subtract(a, b);
  }
  return null;
};

// calculator.test.js
import { calculate } from './calculator';
import * as math from './math';

// 전체 math 모듈을 모킹합니다
jest.mock('./math');

test('should perform addition using mocked math add', () => {
  // 'add' 함수에 대한 모의 구현을 제공합니다
  math.add.mockImplementation((a, b) => a + b + 10); // 결과에 10을 더합니다
  math.subtract.mockReturnValue(5); // subtract도 모킹합니다

  const result = calculate('add', 5, 3);

  expect(math.add).toHaveBeenCalledWith(5, 3);
  expect(result).toBe(18); // 5 + 3 + 10

  const subResult = calculate('subtract', 10, 2);
  expect(math.subtract).toHaveBeenCalledWith(10, 2);
  expect(subResult).toBe(5);
});

스냅샷 테스트: UI 및 구성 보존하기

스냅샷 테스트는 컴포넌트나 구성의 출력을 캡처하는 강력한 기능입니다. 특히 UI 테스트나 복잡한 데이터 구조를 검증하는 데 유용합니다.

스냅샷 테스트 작동 방식

스냅샷 테스트가 처음 실행되면 Jest는 테스트된 값의 직렬화된 표현을 포함하는 .snap 파일을 생성합니다. 이후 실행 시 Jest는 현재 출력을 저장된 스냅샷과 비교합니다. 만약 다르면 테스트가 실패하여 의도하지 않은 변경 사항을 알려줍니다. 이는 여러 지역이나 로케일에서 UI 컴포넌트의 회귀를 감지하는 데 매우 유용합니다.

예시: React 컴포넌트 스냅샷 찍기

React 컴포넌트가 있다고 가정해 봅시다:

// UserProfile.js
import React from 'react';

const UserProfile = ({ name, email, isActive }) => (
  <div>
    <h2>{name}</h2>
    <p><strong>Email:</strong> {email}</p>
    <p><strong>Status:</strong> {isActive ? 'Active' : 'Inactive'}</p>
  </div>
);

export default UserProfile;

// UserProfile.test.js
import React from 'react';
import renderer from 'react-test-renderer'; // React 컴포넌트 스냅샷을 위해
import UserProfile from './UserProfile';

test('renders UserProfile correctly', () => {
  const user = {
    name: 'Jane Doe',
    email: 'jane.doe@example.com',
    isActive: true,
  };
  const component = renderer.create(
    <UserProfile {...user} />
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

test('renders inactive UserProfile correctly', () => {
  const user = {
    name: 'John Smith',
    email: 'john.smith@example.com',
    isActive: false,
  };
  const component = renderer.create(
    <UserProfile {...user} />
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot('비활성 사용자 프로필'); // 이름 있는 스냅샷
});

테스트를 실행하면 Jest는 UserProfile.test.js.snap 파일을 생성합니다. 컴포넌트를 업데이트할 때 변경 사항을 검토하고 Jest를 --updateSnapshot 또는 -u 플래그와 함께 실행하여 스냅샷을 업데이트해야 할 수도 있습니다.

스냅샷 테스트 모범 사례

커스텀 매처: 테스트 가독성 향상하기

Jest의 내장 매처는 광범위하지만, 때로는 다루지 않는 특정 조건을 단언해야 할 때가 있습니다. 커스텀 매처를 사용하면 자신만의 단언 로직을 생성하여 테스트를 더 표현력 있고 가독성 있게 만들 수 있습니다.

커스텀 매처 생성하기

Jest의 expect 객체를 자신만의 매처로 확장할 수 있습니다.

예시: 유효한 이메일 형식 확인하기

Jest 설정 파일(예: jest.setup.js, jest.config.js에서 구성)에서:

// jest.setup.js

expect.extend({
  toBeValidEmail(received) {
    const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
    const pass = emailRegex.test(received);

    if (pass) {
      return {
        message: () => `예상: ${received}는(은) 유효한 이메일이 아니어야 합니다`,
        pass: true,
      };
    } else {
      return {
        message: () => `예상: ${received}는(은) 유효한 이메일이어야 합니다`,
        pass: false,
      };
    }
  },
});

// jest.config.js에서
// module.exports = { setupFilesAfterEnv: ['/jest.setup.js'] };

테스트 파일에서:

// validation.test.js

test('should validate email formats', () => {
  expect('test@example.com').toBeValidEmail();
  expect('invalid-email').not.toBeValidEmail();
  expect('another.test@sub.domain.co.uk').toBeValidEmail();
});

커스텀 매처의 이점

비동기 작업 테스트하기

자바스크립트는 비동기성이 강합니다. Jest는 프로미스와 async/await 테스트를 위한 훌륭한 지원을 제공합니다.

async/await 사용하기

이는 비동기 코드를 테스트하는 가장 현대적이고 가독성이 높은 방법입니다.

예시: 비동기 함수 테스트하기

// dataService.js
export const fetchUserData = async (userId) => {
  // 지연 후 데이터 가져오기 시뮬레이션
  await new Promise(resolve => setTimeout(resolve, 50));
  if (userId === 1) {
    return { id: 1, name: 'Alice' };
  } else {
    throw new Error('사용자를 찾을 수 없습니다');
  }
};

// dataService.test.js
import { fetchUserData } from './dataService';

test('fetches user data correctly', async () => {
  const user = await fetchUserData(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
});

test('throws error for non-existent user', async () => {
  await expect(fetchUserData(2)).rejects.toThrow('사용자를 찾을 수 없습니다');
});

.resolves.rejects 사용하기

이 매처들은 프로미스 해결 및 거부를 간단하게 테스트할 수 있게 해줍니다.

예시: .resolves/.rejects 사용하기

// dataService.test.js (continued)

test('fetches user data with .resolves', () => {
  return expect(fetchUserData(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});

test('throws error for non-existent user with .rejects', () => {
  return expect(fetchUserData(2)).rejects.toThrow('사용자를 찾을 수 없습니다');
});

타이머 처리하기

setTimeout이나 setInterval을 사용하는 함수를 위해 Jest는 타이머 제어 기능을 제공합니다.

예시: 타이머 제어하기

// delayedGreeter.js
export const greetAfterDelay = (name, callback) => {
  setTimeout(() => {
    callback(`Hello, ${name}!`);
  }, 1000);
};

// delayedGreeter.test.js
import { greetAfterDelay } from './delayedGreeter';

jest.useFakeTimers(); // 가짜 타이머 활성화

test('greets after delay', () => {
  const mockCallback = jest.fn();
  greetAfterDelay('World', mockCallback);

  // 타이머를 1000ms 만큼 진행시킵니다
  jest.advanceTimersByTime(1000);

  expect(mockCallback).toHaveBeenCalledTimes(1);
  expect(mockCallback).toHaveBeenCalledWith('안녕하세요, World!');
});

// 다른 곳에서 필요하다면 실제 타이머로 복원합니다
jest.useRealTimers();

테스트 구성 및 구조

테스트 스위트가 커짐에 따라 구성은 유지보수성에 매우 중요해집니다.

Describe 블록과 It 블록

관련 테스트를 그룹화하기 위해 describe를 사용하고 개별 테스트 케이스에는 it(또는 test)을 사용합니다. 이 구조는 애플리케이션의 모듈성을 반영합니다.

예시: 구조화된 테스트

describe('User Authentication Service', () => {
  let authService;

  beforeEach(() => {
    // 각 테스트 전에 모크나 서비스 인스턴스를 설정합니다
    authService = require('./authService');
    jest.spyOn(authService, 'login').mockImplementation(() => Promise.resolve({ token: 'fake_token' }));
  });

  afterEach(() => {
    // 모크를 정리합니다
    jest.restoreAllMocks();
  });

  describe('login functionality', () => {
    it('유효한 자격 증명으로 사용자가 성공적으로 로그인해야 합니다', async () => {
      const result = await authService.login('user@example.com', 'password123');
      expect(result.token).toBeDefined();
      // ... 추가 단언 ...
    });

    it('유효하지 않은 자격 증명으로 로그인이 실패해야 합니다', async () => {
      jest.spyOn(authService, 'login').mockRejectedValue(new Error('유효하지 않은 자격 증명'));
      await expect(authService.login('user@example.com', 'wrong_password')).rejects.toThrow('유효하지 않은 자격 증명');
    });
  });

  describe('logout functionality', () => {
    it('should clear user session', async () => {
      // 로그아웃 로직 테스트...
    });
  });
});

설정 및 해제 훅

이러한 훅은 테스트 간에 모의 데이터 설정, 데이터베이스 연결 또는 리소스 정리에 필수적입니다.

글로벌 사용자를 위한 테스트

글로벌 사용자를 위한 애플리케이션을 개발할 때 테스트 고려 사항이 확장됩니다:

국제화(i18n) 및 현지화(l10n)

UI와 메시지가 다른 언어 및 지역 형식에 올바르게 적응하는지 확인하십시오.

예시: 지역화된 날짜 형식 테스트하기

// dateUtils.js
export const formatLocalizedDate = (date, locale) => {
  return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' }).format(date);
};

// dateUtils.test.js
import { formatLocalizedDate } from './dateUtils';

test('formats date correctly for US locale', () => {
  const date = new Date(2023, 10, 15); // 2023년 11월 15일
  expect(formatLocalizedDate(date, 'en-US')).toBe('11/15/2023');
});

test('formats date correctly for German locale', () => {
  const date = new Date(2023, 10, 15);
  expect(formatLocalizedDate(date, 'de-DE')).toBe('15.11.2023');
});

시간대 인식

애플리케이션이 다양한 시간대를 어떻게 처리하는지 테스트하십시오. 특히 스케줄링이나 실시간 업데이트와 같은 기능의 경우 더욱 그렇습니다. 시스템 시계를 모킹하거나 시간대를 추상화하는 라이브러리를 사용하는 것이 유용할 수 있습니다.

데이터의 문화적 뉘앙스

숫자, 통화 및 기타 데이터 표현이 문화에 따라 어떻게 인식되거나 기대될 수 있는지 고려하십시오. 커스텀 매처가 특히 여기에서 유용할 수 있습니다.

고급 기술 및 전략

테스트 주도 개발(TDD) 및 행동 주도 개발(BDD)

Jest는 TDD(Red-Green-Refactor) 및 BDD(Given-When-Then) 방법론과 잘 맞습니다. 구현 코드를 작성하기 전에 원하는 동작을 설명하는 테스트를 작성하십시오. 이는 코드가 처음부터 테스트 용이성을 염두에 두고 작성되도록 보장합니다.

Jest를 사용한 통합 테스트

Jest는 단위 테스트에 뛰어나지만 통합 테스트에도 사용될 수 있습니다. 의존성을 덜 모킹하거나 Jest의 runInBand 옵션과 같은 도구를 사용하는 것이 도움이 될 수 있습니다.

예시: API 상호작용 테스트 (단순화됨)

// apiService.js
import axios from 'axios';

const API_BASE_URL = 'https://api.example.com';

export const createProduct = async (productData) => {
  const response = await axios.post(`${API_BASE_URL}/products`, productData);
  return response.data;
};

// apiService.test.js (Integration test)
import axios from 'axios';
import { createProduct } from './apiService';

// 네트워크 계층을 제어하기 위해 통합 테스트용 axios를 모킹합니다
jest.mock('axios');

test('creates a product via API', async () => {
  const mockProduct = { id: 1, name: 'Gadget' };
  const responseData = { success: true, product: mockProduct };

  axios.post.mockResolvedValue({
    data: responseData,
    status: 201,
    headers: { 'content-type': 'application/json' },
  });

  const newProductData = { name: 'Gadget', price: 99.99 };
  const result = await createProduct(newProductData);

  expect(axios.post).toHaveBeenCalledWith(`${process.env.API_BASE_URL || 'https://api.example.com'}/products`, newProductData);
  expect(result).toEqual(responseData);
});

병렬 처리 및 구성

Jest는 실행 속도를 높이기 위해 테스트를 병렬로 실행할 수 있습니다. jest.config.js에서 이를 구성하십시오. 예를 들어, maxWorkers를 설정하면 병렬 프로세스의 수를 제어합니다.

커버리지 보고서

Jest의 내장 커버리지 보고를 사용하여 코드베이스에서 테스트되지 않은 부분을 식별하십시오. --coverage 플래그와 함께 테스트를 실행하여 상세 보고서를 생성하십시오.

jest --coverage

커버리지 보고서를 검토하면 고급 테스트 패턴이 국제화 및 현지화 코드 경로를 포함한 중요한 로직을 효과적으로 커버하고 있는지 확인하는 데 도움이 됩니다.

결론

고급 Jest 테스트 패턴을 마스터하는 것은 전 세계 사용자를 위해 신뢰할 수 있고 유지보수 가능하며 고품질의 소프트웨어를 구축하는 데 있어 중요한 단계입니다. 모킹, 스냅샷 테스트, 커스텀 매처 및 비동기 테스트 기술을 효과적으로 활용함으로써 테스트 스위트의 견고성을 향상시키고 다양한 시나리오와 지역에 걸쳐 애플리케이션의 동작에 대한 더 큰 확신을 얻을 수 있습니다. 이러한 패턴을 수용하면 전 세계 개발팀이 탁월한 사용자 경험을 제공할 수 있습니다.

지금 바로 워크플로우에 이러한 고급 기술을 도입하여 자바스크립트 테스트 관행을 한 단계 끌어올리십시오.

Jest: 견고한 소프트웨어를 위한 고급 테스트 패턴 | MLOG