
Jest 핵심 문법 및 시니어 개발자 테스트 설계 전략
핵심 문법 완전 정리
1. 기본 테스트 구조
// 테스트 그룹화
describe('Calculator', () => {
// 개별 테스트 케이스
test('should add two numbers', () => {
expect(add(2, 3)).toBe(5);
});
// it은 test의 별칭
it('should subtract correctly', () => {
expect(subtract(5, 3)).toBe(2);
});
});
2. 핵심 Matchers
// 동등성 검사
expect(value).toBe(expected); // Object.is() 사용 (참조 비교)
expect(value).toEqual(expected); // 깊은 동등성 비교
expect(value).toStrictEqual(expected); // undefined 필드까지 엄격 비교
// Truthiness
expect(value).toBeTruthy(); // if문에서 true로 평가
expect(value).toBeFalsy(); // if문에서 false로 평가
expect(value).toBeNull(); // null만 매치
expect(value).toBeUndefined(); // undefined만 매치
expect(value).toBeDefined(); // undefined가 아님
// 숫자
expect(value).toBeGreaterThan(3); // > 3
expect(value).toBeGreaterThanOrEqual(3); // >= 3
expect(value).toBeCloseTo(0.3); // 부동소수점 근사치
// 문자열
expect(string).toMatch(/pattern/); // 정규식 매치
expect(string).toContain('substring'); // 부분 문자열 포함
// 배열/반복가능객체
expect(array).toContain(item); // 요소 포함
expect(array).toHaveLength(number); // 길이 확인
// 객체
expect(object).toHaveProperty('key'); // 프로퍼티 존재
expect(object).toHaveProperty('key', value); // 프로퍼티 값까지 확인
// 예외
expect(() => {
throw new Error('Something bad');
}).toThrow(); // 예외 발생 확인
expect(() => {
throw new Error('Something bad');
}).toThrow('Something bad'); // 특정 메시지 확인
// 비동기
await expect(promise).resolves.toBe(value); // Promise 성공
await expect(promise).rejects.toThrow(); // Promise 실패
3. 모킹 시스템
// Mock 함수 생성
const mockFn = jest.fn();
const mockFnWithReturn = jest.fn(() => 'mocked value');
const mockFnOnce = jest.fn()
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call');
// Mock 함수 검증
expect(mockFn).toHaveBeenCalled(); // 호출됨
expect(mockFn).toHaveBeenCalledTimes(2); // 호출 횟수
expect(mockFn).toHaveBeenCalledWith(arg1, arg2); // 특정 인자로 호출
expect(mockFn).toHaveBeenLastCalledWith(arg); // 마지막 호출 인자
// 모듈 모킹
jest.mock('./module', () => ({
default: jest.fn(),
namedExport: jest.fn()
}));
// 부분 모킹
jest.mock('./module', () => ({
...jest.requireActual('./module'),
specificFunction: jest.fn()
}));
// 자동 모킹
jest.mock('./module'); // 모든 export를 jest.fn()으로 대체
4. 생명주기 훅
describe('Test Suite', () => {
// 모든 테스트 전에 한 번
beforeAll(async () => {
await setupDatabase();
});
// 각 테스트 전에
beforeEach(() => {
jest.clearAllMocks();
setupTestData();
});
// 각 테스트 후에
afterEach(() => {
cleanupTestData();
});
// 모든 테스트 후에 한 번
afterAll(async () => {
await teardownDatabase();
});
});
5. 고급 테스트 패턴
// 매개변수화된 테스트
describe.each([
[1, 2, 3],
[2, 3, 5],
[3, 4, 7]
])('add(%i, %i)', (a, b, expected) => {
test(`returns ${expected}`, () => {
expect(add(a, b)).toBe(expected);
});
});
// 조건부 테스트
test.skipIf(process.env.NODE_ENV === 'production')('dev only test', () => {
// 개발 환경에서만 실행
});
// 실패 예상 테스트
test.failing('known broken feature', () => {
expect(brokenFunction()).toBe(expectedValue);
});
// 비동기 테스트
test('async operation', async () => {
const result = await asyncFunction();
expect(result).toBe(expected);
});
// 타임아웃과 재시도
jest.setTimeout(10000); // 글로벌 타임아웃
jest.retryTimes(3); // 실패 시 재시도
test('with custom timeout', async () => {
// 개별 테스트 타임아웃
}, 15000);
시니어 개발자 테스트 설계 전략
1. 테스트 아키텍처 설계
1.1 테스트 피라미드 적용
// 🏗️ 레이어별 테스트 분배 전략
/**
* E2E Tests (5-10%) ← 느리지만 중요한 사용자 시나리오
* Integration (15-25%) ← 컴포넌트 간 상호작용
* Unit Tests (70-80%) ← 빠르고 안정적인 기초
*/
// 🎯 단위 테스트: 순수 함수, 유틸리티, 비즈니스 로직
describe('BusinessLogic', () => {
test('calculateDiscount applies correct percentage', () => {
const order = { total: 100, customerType: 'premium' };
expect(calculateDiscount(order)).toBe(15);
});
});
// 🔗 통합 테스트: 서비스 레이어, API 호출, 데이터베이스
describe('UserService Integration', () => {
test('creates user with hashed password', async () => {
const user = await userService.create('john', 'password123');
expect(user.password).not.toBe('password123');
expect(await bcrypt.compare('password123', user.password)).toBe(true);
});
});
// 🌐 E2E 테스트: 핵심 사용자 플로우만
describe('Critical User Journey', () => {
test('user can complete purchase flow', async () => {
await loginAsUser();
await addItemToCart('premium-product');
await proceedToCheckout();
await completePayment();
await expect(page).toHaveURL(/order-confirmation/);
});
});
1.2 테스트 조직화 패턴
// 📁 디렉토리 구조
/*
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx ← 단위 테스트
│ │ └── Button.integration.test.tsx ← 통합 테스트
├── services/
│ └── __tests__/ ← 서비스 테스트 집중
├── utils/
│ └── __tests__/ ← 유틸리티 테스트
└── __tests__/
├── e2e/ ← E2E 테스트
├── fixtures/ ← 테스트 데이터
└── helpers/ ← 테스트 유틸리티
*/
// 🏗️ 테스트 빌더 패턴
class UserBuilder {
private userData: Partial<User> = {};
withEmail(email: string) {
this.userData.email = email;
return this;
}
withRole(role: UserRole) {
this.userData.role = role;
return this;
}
build(): User {
return {
id: faker.datatype.uuid(),
email: faker.internet.email(),
role: 'user',
...this.userData
};
}
}
// 사용법
const adminUser = new UserBuilder()
.withEmail('admin@test.com')
.withRole('admin')
.build();
2. 모킹 전략
2.1 의존성 주입과 모킹
// ❌ 피해야 할 패턴: 하드코딩된 의존성
class UserService {
async createUser(data: UserData) {
const hashedPassword = await bcrypt.hash(data.password, 10); // 직접 의존
return this.userRepository.save({ ...data, password: hashedPassword });
}
}
// ✅ 권장 패턴: 의존성 주입
class UserService {
constructor(
private userRepository: UserRepository,
private passwordHasher: PasswordHasher // 추상화된 의존성
) {}
async createUser(data: UserData) {
const hashedPassword = await this.passwordHasher.hash(data.password);
return this.userRepository.save({ ...data, password: hashedPassword });
}
}
// 테스트에서 쉬운 모킹
describe('UserService', () => {
let userService: UserService;
let mockRepository: jest.Mocked<UserRepository>;
let mockHasher: jest.Mocked<PasswordHasher>;
beforeEach(() => {
mockRepository = {
save: jest.fn(),
findById: jest.fn()
} as jest.Mocked<UserRepository>;
mockHasher = {
hash: jest.fn().mockResolvedValue('hashed_password')
} as jest.Mocked<PasswordHasher>;
userService = new UserService(mockRepository, mockHasher);
});
});
2.2 스마트 모킹 전략
// 🎯 레이어별 모킹 가이드라인
describe('Service Layer Testing', () => {
// ✅ 외부 API는 항상 모킹
beforeEach(() => {
nock('https://api.external.com')
.get('/users/123')
.reply(200, { id: 123, name: 'John' });
});
// ✅ 데이터베이스는 통합 테스트에서만 실제 사용
const mockDb = {
user: {
create: jest.fn().mockImplementation(async (data) => ({
id: faker.datatype.number(),
...data
}))
}
};
// ✅ 시간 기반 로직은 가짜 타이머 사용
test('schedules job correctly', () => {
jest.useFakeTimers();
const mockCallback = jest.fn();
scheduleJob(mockCallback, 1000);
jest.advanceTimersByTime(1000);
expect(mockCallback).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});
});
3. 테스트 품질 관리
3.1 AAA 패턴 엄격 적용
// ✅ 명확한 AAA 구조
describe('OrderCalculator', () => {
test('applies senior discount correctly', () => {
// Arrange - 테스트 데이터 준비
const customer = createCustomer({ age: 70, membershipYears: 5 });
const items = [
createItem({ price: 100, category: 'electronics' }),
createItem({ price: 50, category: 'books' })
];
const calculator = new OrderCalculator();
// Act - 테스트할 동작 실행
const result = calculator.calculateTotal(items, customer);
// Assert - 결과 검증 (여러 어설션도 OK, 같은 동작에 대한 것이라면)
expect(result.subtotal).toBe(150);
expect(result.discount).toBe(15); // 10% 시니어 할인
expect(result.total).toBe(135);
expect(result.appliedDiscounts).toContain('SENIOR_DISCOUNT');
});
});
3.2 의미 있는 테스트 작성
// ❌ 구현 세부사항 테스트
test('calls setState with correct parameters', () => {
const component = render(<Counter />);
fireEvent.click(screen.getByText('Increment'));
// setState 호출을 테스트하는 것은 의미없음
});
// ✅ 사용자 관점에서 테스트
test('increments counter when increment button is clicked', () => {
render(<Counter initialValue={5} />);
const incrementButton = screen.getByRole('button', { name: /increment/i });
fireEvent.click(incrementButton);
expect(screen.getByText('6')).toBeInTheDocument();
});
// ✅ 비즈니스 규칙 테스트
test('prevents overdraft when insufficient balance', () => {
const account = new BankAccount(100);
expect(() => account.withdraw(150)).toThrow('Insufficient balance');
expect(account.getBalance()).toBe(100); // 잔액 변경 없음 확인
});
4. 고급 테스트 패턴
4.1 상태 기반 vs 상호작용 기반 테스트
// 🎯 상태 기반 테스트 (권장: 리팩토링에 강함)
describe('ShoppingCart', () => {
test('adds item correctly', () => {
const cart = new ShoppingCart();
const item = createItem({ id: '1', price: 10 });
cart.addItem(item, 2);
expect(cart.getTotalItems()).toBe(2);
expect(cart.getTotalPrice()).toBe(20);
expect(cart.getItems()).toContainEqual(
expect.objectContaining({ id: '1', quantity: 2 })
);
});
});
// ⚠️ 상호작용 기반 테스트 (필요한 경우에만)
describe('OrderService', () => {
test('sends confirmation email after successful order', async () => {
const mockEmailService = jest.mocked(emailService);
const orderService = new OrderService(mockEmailService);
await orderService.createOrder(validOrderData);
expect(mockEmailService.sendConfirmationEmail).toHaveBeenCalledWith(
expect.objectContaining({
to: validOrderData.customerEmail,
orderNumber: expect.any(String)
})
);
});
});
4.2 에러 시나리오 철저 테스트
describe('PaymentProcessor', () => {
// ✅ 다양한 실패 시나리오 커버
test.each([
['invalid card', { cardNumber: '1234', error: 'Invalid card number' }],
['expired card', { cardNumber: '4111111111111111', expiryDate: '01/20', error: 'Card expired' }],
['insufficient funds', { cardNumber: '4111111111111111', error: 'Insufficient funds' }]
])('handles %s gracefully', async (scenario, { error, ...cardData }) => {
const processor = new PaymentProcessor();
await expect(processor.processPayment({
amount: 100,
...cardData
})).rejects.toThrow(error);
});
// ✅ 네트워크 에러 처리
test('retries on network failure', async () => {
const mockApiCall = jest.fn()
.mockRejectedValueOnce(new NetworkError('Connection timeout'))
.mockRejectedValueOnce(new NetworkError('Connection timeout'))
.mockResolvedValueOnce({ success: true, transactionId: '123' });
const processor = new PaymentProcessor({ apiCall: mockApiCall, maxRetries: 3 });
const result = await processor.processPayment(validPaymentData);
expect(result.success).toBe(true);
expect(mockApiCall).toHaveBeenCalledTimes(3);
});
});
5. 성능 및 안정성
5.1 테스트 성능 최적화
// ✅ 테스트 실행 속도 개선
describe('Large Test Suite', () => {
// 비용이 큰 설정은 describe 레벨에서
let expensiveResource: ExpensiveResource;
beforeAll(async () => {
expensiveResource = await createExpensiveResource();
});
afterAll(async () => {
await expensiveResource.cleanup();
});
// 각 테스트마다 리셋만
beforeEach(() => {
expensiveResource.reset();
});
// 병렬 실행 가능한 테스트 설계
test.concurrent('independent test 1', async () => {
const result = await independentOperation1();
expect(result).toBeDefined();
});
test.concurrent('independent test 2', async () => {
const result = await independentOperation2();
expect(result).toBeDefined();
});
});
5.2 플레이키 테스트 방지
// ❌ 플레이키 테스트 패턴들
test('flaky test', async () => {
setTimeout(() => expect(something).toBe(true), 100); // 타이밍 의존
expect(Math.random() > 0.5).toBe(true); // 랜덤 결과
expect(new Date().getTime()).toBeGreaterThan(startTime); // 시간 의존
});
// ✅ 안정적인 테스트 패턴
test('stable test', async () => {
// 명확한 대기 조건
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
// 가짜 타이머 사용
jest.useFakeTimers();
const callback = jest.fn();
scheduleCallback(callback, 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
jest.useRealTimers();
// 고정된 시드 사용
const seededRandom = new SeededRandom('test-seed');
const result = generateWithRandom(seededRandom);
expect(result).toBe(expectedValue);
});
안티패턴 및 베스트 프랙티스
피해야 할 안티패턴12
// ❌ 1. 과도한 모킹
describe('OverMockedTest', () => {
test('everything is mocked', () => {
const mockA = jest.fn();
const mockB = jest.fn();
const mockC = jest.fn();
// 실제 비즈니스 로직은 테스트되지 않음
});
});
// ❌ 2. 테스트에서 로직 사용
test('conditional assertions', () => {
const result = calculate(input);
if (result > 0) {
expect(result).toBeGreaterThan(0);
} else {
expect(result).toBeLessThanOrEqual(0);
}
});
// ❌ 3. 구현 세부사항 의존
test('tests implementation details', () => {
const spy = jest.spyOn(component, 'privateMethod');
component.publicMethod();
expect(spy).toHaveBeenCalled();
});
권장 베스트 프랙티스34
// ✅ 1. 명확한 테스트 이름
describe('UserRegistration', () => {
describe('when user provides valid data', () => {
test('creates user account and sends welcome email', () => {
// 테스트 구현
});
});
describe('when email is already taken', () => {
test('returns validation error without creating duplicate', () => {
// 테스트 구현
});
});
});
// ✅ 2. 테스트 격리
describe('IsolatedTests', () => {
beforeEach(() => {
jest.clearAllMocks();
resetTestDatabase();
});
test('test 1 does not affect test 2', () => {
// 독립적인 테스트
});
});
// ✅ 3. 적절한 추상화
const createTestUser = (overrides = {}) => ({
id: faker.datatype.uuid(),
email: faker.internet.email(),
name: faker.name.fullName(),
...overrides
});
test('user can update profile', () => {
const user = createTestUser({ name: 'Original Name' });
const result = updateUserProfile(user, { name: 'New Name' });
expect(result.name).toBe('New Name');
});
결론
시니어 개발자의 테스트 설계는 단순히 코드를 검증하는 것을 넘어서 시스템의 안정성과 유지보수성을 보장하는 아키텍처를 구축하는 것이다.
핵심 원칙:
- 테스트 피라미드 준수로 비용 효율적인 테스트 전략 수립
- 의존성 주입과 스마트 모킹으로 테스트 가능한 코드 설계
- AAA 패턴 및 사용자 관점 테스트로 의미 있는 검증
- 에러 시나리오와 엣지 케이스 철저 커버리지
- 플레이키 테스트 방지와 성능 최적화로 안정적인 테스트 스위트 구축
이러한 전략을 통해 개발팀은 빠른 피드백, 높은 코드 신뢰성, 안전한 리팩토링을 실현할 수 있으며, 궁극적으로 품질 높은 소프트웨어 제품을 지속적으로 배포할 수 있다.
⁂
Footnotes
-
https://dzone.com/articles/testing-the-untestable-and-other-anti-patterns ↩
-
https://builders.travelperk.com/recipes-to-write-better-jest-tests-with-the-react-testing-library-part-1-670aaf3451d1 ↩
-
https://www.linkedin.com/pulse/best-practices-testing-jest-part-1-lucas-mendonça-5bref ↩
-
https://geekyants.com/blog/writing-effective-unit-tests-best-practices ↩