소프트웨어 테스트 자동화 완벽 가이드 (단위·통합·E2E 테스트)
테스트 피라미드
▲
/\
/ \ E2E 테스트 (UI 테스트)
/ \ 5-10%
/──────\
/ \ 통합 테스트 (Integration)
/ \ 15-20%
/────────────\
/ \ 단위 테스트 (Unit)
/________________\ 70-80%핵심 규칙:
- 단위 테스트로 기초 다지기
- 통합 테스트로 연결 확인
- E2E 테스트로 사용자 시나리오 검증
단위 테스트 (Unit Test)
Jest (JavaScript 표준)
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');
describe('sum function', () => {
test('adds two numbers', () => {
expect(sum(2, 3)).toBe(5);
});
test('adds negative numbers', () => {
expect(sum(-1, -1)).toBe(-2);
});
test('adds zero', () => {
expect(sum(5, 0)).toBe(5);
});
});실행:
npm test
# 3 passed in 0.5sJest 주요 매처 (Assertions)
// 동등성
expect(2 + 2).toBe(4); // 정확히 같음
expect({a: 1}).toEqual({a: 1}); // 값이 같음
// 불린
expect(true).toBeTruthy();
expect(false).toBeFalsy();
// 배열
expect([1, 2, 3]).toContain(2);
// 예외
expect(() => {
throw new Error('Boom!');
}).toThrow('Boom!');
// 비동기
test('async test', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});통합 테스트 (Integration Test)
API 테스트
// userApi.test.js
describe('User API', () => {
test('GET /users returns all users', async () => {
const response = await fetch('/api/users');
const users = await response.json();
expect(response.status).toBe(200);
expect(Array.isArray(users)).toBe(true);
expect(users.length).toBeGreaterThan(0);
});
test('POST /users creates new user', async () => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John', email: 'john@example.com' })
});
expect(response.status).toBe(201);
const user = await response.json();
expect(user.id).toBeDefined();
expect(user.name).toBe('John');
});
});데이터베이스 통합 테스트
describe('User Repository', () => {
beforeEach(async () => {
// 각 테스트 전에 데이터베이스 초기화
await db.clear();
});
test('save and retrieve user', async () => {
const user = { name: 'Jane', email: 'jane@example.com' };
const savedUser = await userRepo.save(user);
const retrieved = await userRepo.findById(savedUser.id);
expect(retrieved.name).toBe('Jane');
});
afterEach(async () => {
// 테스트 후 정리
await db.close();
});
});E2E 테스트 (End-to-End)
Playwright (권장)
// login.spec.js
const { test, expect } = require('@playwright/test');
test('user can login successfully', async ({ page }) => {
// 1. 로그인 페이지 접속
await page.goto('https://example.com/login');
// 2. 이메일 입력
await page.fill('input[name="email"]', 'user@example.com');
// 3. 비밀번호 입력
await page.fill('input[name="password"]', 'password123');
// 4. 로그인 버튼 클릭
await page.click('button:has-text("Sign in")');
// 5. 로그인 성공 확인 (대시보드로 리다이렉트)
await expect(page).toHaveURL('https://example.com/dashboard');
// 6. 환영 메시지 확인
const welcomeText = await page.textContent('h1');
expect(welcomeText).toContain('Welcome');
});
test('purchase flow', async ({ page }) => {
// 상품 추가
await page.goto('https://example.com/shop');
await page.click('button:has-text("Add to Cart")');
// 장바구니 확인
await page.click('text=Cart');
await expect(page.locator('text=Total: $99.99')).toBeVisible();
// 결제
await page.click('button:has-text("Checkout")');
await page.fill('input[name="card"]', '4111111111111111');
await page.click('button:has-text("Pay")');
// 완료 확인
await expect(page).toContainText('Order Complete');
});Selenium (레거시 지원)
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://example.com")
# 요소 찾기
search_box = driver.find_element(By.NAME, "q")
search_box.send_keys("selenium")
search_box.submit()
# 결과 확인
assert "No results" not in driver.page_source
driver.quit()테스트 커버리지
커버리지란?
테스트가 소스 코드의 몇 %를 실행하는가?
Line Coverage: 코드 라인의 몇 %가 실행되었는가?
Branch Coverage: 조건문의 모든 분기가 테스트되었는가?
Function Coverage: 모든 함수가 호출되었는가?Jest에서 커버리지 확인
npm test -- --coverage
# 출력:
# ──────────────────────────────────────────
# File | % Stmts | % Branch | % Funcs
# ──────────────────────────────────────────
# sum.js | 100 | 100 | 100
# userApi.js | 85 | 80 | 90
# ──────────────────────────────────────────목표 커버리지
웹사이트: 70-80% (모든 기능 필수)
라이브러리: 90%+ (높은 신뢰성)
게임: 50-70% (빠른 반복)주의: 100% 커버리지는 필요 없습니다. 가장 중요한 기능에 집중하세요.
Mock과 Stub
Mock (동작 검증)
const mockCallback = jest.fn();
// 함수 호출
myFunction(mockCallback);
// 호출 확인
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback).toHaveBeenCalledWith(1, 2);
expect(mockCallback).toHaveBeenCalledTimes(1);Stub (외부 의존성 대체)
// API 호출을 Stub으로 대체
jest.mock('axios');
const axios = require('axios');
test('fetch user data', async () => {
// 외부 API 호출 대신 mock 데이터 반환
axios.get.mockResolvedValue({
data: { id: 1, name: 'John' }
});
const user = await fetchUser(1);
expect(user.name).toBe('John');
});CI/CD에 테스트 통합
GitHub Actions 예시
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- run: npm test -- --coverage
- run: npm run test:e2e동작:
테스트 베스트 프랙티스
1. 이름이 명확해야 함
// ❌ 나쁜 예
test('test login', () => { ... });
// ✅ 좋은 예
test('should redirect to dashboard after successful login', () => { ... });2. 한 가지만 테스트
// ❌ 나쁜 예
test('user login and profile update', () => {
// 로그인 테스트
// 프로필 업데이트 테스트
});
// ✅ 좋은 예
test('user login with valid credentials', () => { ... });
test('user can update profile after login', () => { ... });3. AAA 패턴 (Arrange, Act, Assert)
test('calculates total price correctly', () => {
// Arrange: 테스트 준비
const items = [
{ name: 'Apple', price: 1 },
{ name: 'Banana', price: 2 }
];
// Act: 동작 실행
const total = calculateTotal(items);
// Assert: 결과 검증
expect(total).toBe(3);
});자주 묻는 질문
> Q. 모든 코드를 테스트해야 하나요?
A. 아닙니다. 비즈니스 로직과 API는 필수. 간단한 UI는 선택적.
> Q. 테스트 코드 작성 시간이 더 오래 걸리지 않나요?
A. 초기엔 길지만, 장기적으로 디버깅 시간 50% 감소, 유지보수 비용 크게 절감.
> Q. 테스트 우선 개발(TDD)을 해야 하나요?
A. 선택사항입니다. 하지만 습관화하면 코드 품질 크게 향상.
> Q. 어떤 테스트부터 시작해야 하나요?
A. 비즈니스 로직 단위 테스트 → API 통합 테스트 → 중요 UI E2E 테스트 순서로.
> Q. 테스트 깨지면 어떻게 하나요?
A. 코드 변경 시 테스트도 수정. 테스트는 살아있는 명세서입니다.
결론
자동 테스트는:
초기 비용 ↑
↓
장기 유지보수 비용 ↓
↓
버그 감소 ↓
↓
개발 속도 ↑ (장기적으로)가장 중요한 것: 테스트는 신뢰성입니다. 완벽한 테스트보다 있는 테스트를 유지하세요.
관련 글: 웹개발 로드맵 | API 설계 베스트 프랙티스
핵심 체크리스트
- [ ] 이 글의 핵심 내용을 이해했는가?
- [ ] 나의 상황에 적용할 수 있는 부분은?
- [ ] 추가로 확인할 사항은?