고급 Jest 테스트 패턴을 마스터하여 더 안정적이고 유지보수하기 쉬운 소프트웨어를 구축하세요. 전 세계 개발팀을 위해 모킹, 스냅샷 테스트, 커스텀 매처 등의 기술을 탐색해 보세요.
Jest: 견고한 소프트웨어를 위한 고급 테스트 패턴
오늘날 빠르게 변화하는 소프트웨어 개발 환경에서 코드베이스의 신뢰성과 안정성을 보장하는 것은 무엇보다 중요합니다. Jest는 자바스크립트 테스트의 사실상 표준이 되었지만, 기본적인 단위 테스트를 넘어 애플리케이션에 대한 새로운 차원의 확신을 얻을 수 있습니다. 이 글에서는 전 세계 개발자들을 대상으로 견고한 소프트웨어를 구축하는 데 필수적인 고급 Jest 테스트 패턴에 대해 자세히 알아봅니다.
기본 단위 테스트를 넘어서야 하는 이유
기본 단위 테스트는 개별 구성 요소를 격리하여 검증합니다. 그러나 실제 애플리케이션은 구성 요소가 상호 작용하는 복잡한 시스템입니다. 고급 테스트 패턴은 다음과 같은 작업을 가능하게 하여 이러한 복잡성을 해결합니다:
- 복잡한 의존성을 시뮬레이션합니다.
- UI 변경 사항을 안정적으로 포착합니다.
- 더 표현력 있고 유지보수하기 쉬운 테스트를 작성합니다.
- 테스트 커버리지를 개선하고 통합 지점에 대한 신뢰도를 높입니다.
- 테스트 주도 개발(TDD) 및 행동 주도 개발(BDD) 워크플로우를 촉진합니다.
모킹(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
플래그와 함께 실행하여 스냅샷을 업데이트해야 할 수도 있습니다.
스냅샷 테스트 모범 사례
- UI 컴포넌트 및 구성 파일에 사용: UI 요소가 예상대로 렌더링되고 구성이 의도치 않게 변경되지 않도록 하는 데 이상적입니다.
- 스냅샷을 신중하게 검토: 스냅샷 업데이트를 맹목적으로 수락하지 마십시오. 항상 변경된 내용을 검토하여 수정이 의도적인지 확인해야 합니다.
- 자주 변경되는 데이터에 대한 스냅샷 피하기: 데이터가 빠르게 변경되면 스냅샷이 깨지기 쉽고 과도한 노이즈를 유발할 수 있습니다.
- 이름 있는 스냅샷 사용: 컴포넌트의 여러 상태를 테스트할 때 이름 있는 스냅샷은 더 나은 명확성을 제공합니다.
커스텀 매처: 테스트 가독성 향상하기
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 () => {
// 로그아웃 로직 테스트...
});
});
});
설정 및 해제 훅
beforeAll
:describe
블록의 모든 테스트 전에 한 번 실행됩니다.afterAll
:describe
블록의 모든 테스트 후에 한 번 실행됩니다.beforeEach
:describe
블록의 각 테스트 전에 실행됩니다.afterEach
:describe
블록의 각 테스트 후에 실행됩니다.
이러한 훅은 테스트 간에 모의 데이터 설정, 데이터베이스 연결 또는 리소스 정리에 필수적입니다.
글로벌 사용자를 위한 테스트
글로벌 사용자를 위한 애플리케이션을 개발할 때 테스트 고려 사항이 확장됩니다:
국제화(i18n) 및 현지화(l10n)
UI와 메시지가 다른 언어 및 지역 형식에 올바르게 적응하는지 확인하십시오.
- 지역화된 UI 스냅샷: 스냅샷 테스트를 사용하여 다양한 언어 버전의 UI가 올바르게 렌더링되는지 테스트합니다.
- 로케일 데이터 모킹:
react-intl
또는i18next
와 같은 라이브러리를 모킹하여 다양한 로케일 메시지로 컴포넌트 동작을 테스트합니다. - 날짜, 시간 및 통화 형식 지정: 커스텀 매처를 사용하거나 국제화 라이브러리를 모킹하여 이러한 항목들이 올바르게 처리되는지 테스트합니다. 예를 들어, 독일(DD.MM.YYYY)용으로 형식화된 날짜가 미국(MM/DD/YYYY)용과 다르게 표시되는지 확인합니다.
예시: 지역화된 날짜 형식 테스트하기
// 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 테스트 패턴을 마스터하는 것은 전 세계 사용자를 위해 신뢰할 수 있고 유지보수 가능하며 고품질의 소프트웨어를 구축하는 데 있어 중요한 단계입니다. 모킹, 스냅샷 테스트, 커스텀 매처 및 비동기 테스트 기술을 효과적으로 활용함으로써 테스트 스위트의 견고성을 향상시키고 다양한 시나리오와 지역에 걸쳐 애플리케이션의 동작에 대한 더 큰 확신을 얻을 수 있습니다. 이러한 패턴을 수용하면 전 세계 개발팀이 탁월한 사용자 경험을 제공할 수 있습니다.
지금 바로 워크플로우에 이러한 고급 기술을 도입하여 자바스크립트 테스트 관행을 한 단계 끌어올리십시오.