Testing
테스팅
코드 검증. 유닛, 통합, E2E 테스트. TDD는 테스트 우선.
테스팅
코드 검증. 유닛, 통합, E2E 테스트. TDD는 테스트 우선.
소프트웨어 테스팅은 코드가 의도한 대로 동작하는지 검증하는 과정입니다. 1979년 Glenford Myers의 "The Art of Software Testing"에서 체계화되었으며, 현대 소프트웨어 개발에서 품질 보증의 핵심입니다.
테스트는 범위에 따라 계층을 이룹니다. Unit Test(단위 테스트)는 개별 함수/클래스를, Integration Test(통합 테스트)는 모듈 간 상호작용을, E2E Test(End-to-End)는 전체 시스템을 검증합니다. 테스트 피라미드 원칙에 따라 Unit이 가장 많고, E2E가 가장 적어야 합니다.
TDD(Test-Driven Development)는 테스트를 먼저 작성하고 코드를 구현하는 방법론입니다. Red-Green-Refactor 사이클로 진행되며, 설계를 개선하고 회귀 버그를 방지합니다. BDD(Behavior-Driven Development)는 비즈니스 요구사항을 Given-When-Then 형식으로 테스트합니다.
실무에서는 테스트 커버리지(보통 80% 이상 목표), 테스트 실행 시간(CI에서 5분 이내 권장), 플레이키 테스트(불안정하게 실패하는 테스트) 관리가 중요합니다. Jest, pytest, JUnit, Cypress 등 언어와 목적에 맞는 프레임워크를 선택합니다.
// Jest를 사용한 테스트 예제
// src/utils/calculator.js
export function add(a, b) {
return a + b;
}
export function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// src/utils/calculator.test.js - Unit Test
import { add, divide } from './calculator';
describe('Calculator', () => {
describe('add', () => {
it('두 양수를 더한다', () => {
expect(add(2, 3)).toBe(5);
});
it('음수를 처리한다', () => {
expect(add(-1, 1)).toBe(0);
});
});
describe('divide', () => {
it('두 숫자를 나눈다', () => {
expect(divide(10, 2)).toBe(5);
});
it('0으로 나누면 에러를 던진다', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
// src/services/userService.test.js - Integration Test
import { createUser, getUser } from './userService';
import { db } from '../db';
describe('UserService Integration', () => {
beforeEach(async () => {
await db.migrate.latest();
await db.seed.run();
});
afterEach(async () => {
await db('users').truncate();
});
it('사용자를 생성하고 조회할 수 있다', async () => {
const userData = { name: 'John', email: 'john@example.com' };
const created = await createUser(userData);
const retrieved = await getUser(created.id);
expect(retrieved.name).toBe('John');
expect(retrieved.email).toBe('john@example.com');
});
});
// cypress/e2e/login.cy.js - E2E Test
describe('로그인 플로우', () => {
beforeEach(() => {
cy.visit('/login');
});
it('올바른 자격 증명으로 로그인할 수 있다', () => {
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').type('password123');
cy.get('[data-testid="login-button"]').click();
cy.url().should('include', '/dashboard');
cy.contains('환영합니다').should('be.visible');
});
it('잘못된 비밀번호로 에러를 표시한다', () => {
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').type('wrong');
cy.get('[data-testid="login-button"]').click();
cy.contains('비밀번호가 일치하지 않습니다').should('be.visible');
});
});
시니어: "이번 릴리즈에 E2E 테스트가 자꾸 flaky하게 실패해서 CI 신뢰도가 떨어지고 있어요. 원인 파악해봐야 해요."
주니어: "특정 테스트만 그런가요? 타임아웃 이슈인 것 같은데 cy.wait 대신 assertion 기반 대기로 바꿔볼까요?"
시니어: "좋아요. 그리고 flaky 테스트는 별도 관리하고, 안정화될 때까지 재시도 로직 넣어요."
면접관: "테스트 커버리지에 대해 어떻게 생각하시나요?"
지원자: "커버리지 숫자보다 중요한 비즈니스 로직이 테스트되었는지가 더 중요하다고 생각합니다. 80% 커버리지를 목표로 하되, 핵심 결제나 인증 로직은 100% 가까이 커버합니다. 단순 getter/setter까지 테스트하는 건 비효율적이고, 복잡한 조건 분기와 엣지 케이스에 집중합니다."
리뷰어: "이 테스트에서 모킹이 너무 많아요. 실제 동작과 달라질 수 있어서 통합 테스트 하나 추가하면 좋겠어요."
개발자: "DB 연결하면 느려질 것 같아서 모킹했는데, 테스트 DB 컨테이너 띄워서 통합 테스트 추가하겠습니다."