💻 IT/테크

소프트웨어 테스트 자동화 완벽 가이드 (단위·통합·E2E 테스트)

📅 2025년 11월 26일 ⏱️ 14분 읽기 ✍️ kimyido

테스트 피라미드

▲
          /\
         /  \     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.5s

Jest 주요 매처 (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 설계 베스트 프랙티스

    핵심 체크리스트

    • [ ] 이 글의 핵심 내용을 이해했는가?
    • [ ] 나의 상황에 적용할 수 있는 부분은?
    • [ ] 추가로 확인할 사항은?
    ✍️
    김이도 편집팀
    정확한 정보 전달을 위해 전문 자료와 공식 통계를 기반으로 콘텐츠를 작성합니다. 최신 정보 반영을 위해 주기적으로 업데이트됩니다.
    📅 최종 업데이트: 2025년 11월 26일 · 📧 문의: 연락하기
    💻 IT/테크 카테고리 전체 글 보기 →