Unit Testing

단위 테스팅은 API 설계 과정에서 개별 컴포넌트나 함수를 독립적으로 테스트하여 각 부분이 올바르게 작동하는지 확인하는 과정이다. 일반적으로 개발 단계에서 수행되며, 각 컴포넌트를 격리시켜 정확한 작동을 검증함으로써 API 전체의 안정성에 대한 신뢰도를 높이는 것이 주요 목표이다.

단위 테스팅이 중요한 이유:

API 단위 테스팅의 기본 원칙

  1. FIRST 원칙

    • Fast: 테스트는 빠르게 실행되어야 한다.
    • Independent/Isolated: 각 테스트는 독립적이어야 하며 다른 테스트에 의존하지 않아야 한다.
    • Repeatable: 테스트는 어떤 환경에서도 동일한 결과를 제공해야 한다.
    • Self-validating: 테스트는 스스로 결과를 검증할 수 있어야 한다.
    • Timely: 테스트는 프로덕션 코드 구현 전이나 직후에 작성되어야 한다.
  2. 단일 책임 원칙 (SRP)
    각 테스트는 하나의 기능 또는 메서드만 테스트해야 한다. 이는 테스트가 실패할 경우 문제를 정확히 파악하는 데 도움이 된다.

  3. AAA 패턴

    • Arrange: 테스트에 필요한 환경과 데이터를 설정한다.
    • Act: 테스트하려는 메서드나 함수를 실행한다.
    • Assert: 결과가 예상과 일치하는지 확인한다.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    # Python에서 AAA 패턴 예시
    def test_create_user():
        # Arrange
        user_data = {"username": "testuser", "email": "test@example.com"}
        user_service = UserService()
    
        # Act
        created_user = user_service.create_user(user_data)
    
        # Assert
        assert created_user["username"] == "testuser"
        assert created_user["email"] == "test@example.com"
        assert "id" in created_user
    

API 단위 테스팅 구성 요소

  1. 테스트 대상

    • 컨트롤러/라우터 함수
    • 서비스 레이어 함수
    • 데이터 접근 레이어
    • 유틸리티 함수
    • 미들웨어
  2. 테스트 도구 및 프레임워크
    언어 및 프레임워크별 인기 있는 테스트 도구:
    JavaScript/Node.js:

    • Jest
    • Mocha + Chai
    • Jasmine
      Python:
    • pytest
    • unittest
    • nose
      Java:
    • JUnit
    • TestNG
    • Mockito
  3. 목(Mock) 객체와 스텁(Stub)
    외부 의존성을 대체하는 가짜 객체를 사용하여 테스트 대상을 격리시킨다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    // JavaScript에서 Jest를 사용한 모킹 예시
    jest.mock('../services/database');
    const dbService = require('../services/database');
    
    test('사용자 조회 함수가 올바른 형식의 데이터를 반환해야 함', () => {
      // 데이터베이스 서비스의 응답을 모의(mock)함
      dbService.findUser.mockResolvedValue({
        id: 1,
        username: 'testuser',
        email: 'test@example.com'
      });
    
      // 테스트 대상 함수 실행
      return userController.getUser(1).then(user => {
        // 결과 검증
        expect(user).toHaveProperty('id', 1);
        expect(user).toHaveProperty('username', 'testuser');
        expect(user).toHaveProperty('email', 'test@example.com');
    
        // 데이터베이스 서비스가 올바른 인자로 호출되었는지 확인
        expect(dbService.findUser).toHaveBeenCalledWith(1);
      });
    });
    

API 단위 테스팅 전략

  1. 테스트 케이스 설계

    • 긍정적 테스트 케이스: 정상적인 입력으로 기대한 결과가 나오는지 확인
    • 부정적 테스트 케이스: 잘못된 입력에 대한 적절한 오류 처리 확인
    • 경계값 테스트: 유효한 입력 범위의 경계에서 함수가 올바르게 작동하는지 확인
    • 예외 처리 테스트: 예외 상황에서 함수가 적절하게 반응하는지 확인
  2. 파라미터화된 테스트
    다양한 입력 데이터로 동일한 테스트 로직을 반복 실행한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    # Python의 pytest를 사용한 파라미터화된 테스트 예시
    import pytest
    
    @pytest.mark.parametrize("input_value, expected_output", [
        (1, "1st"),
        (2, "2nd"),
        (3, "3rd"),
        (4, "4th"),
        (21, "21st")
    ])
    def test_ordinal_suffix(input_value, expected_output):
        assert get_ordinal_suffix(input_value) == expected_output
    
  3. 코드 커버리지
    테스트가 코드의 얼마나 많은 부분을 실행하는지 측정한다.
    일반적으로 다음과 같은 유형의 커버리지를 고려한다:

    • 라인 커버리지: 실행된 코드 라인의 비율
    • 분기 커버리지: 실행된 코드 브랜치의 비율
    • 함수 커버리지: 호출된 함수의 비율

RESTful API 단위 테스팅

  1. 컨트롤러 레이어 테스팅
    HTTP 요청과 응답을 처리하는 컨트롤러 함수를 테스트한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    // Express.js 컨트롤러 테스트 예시 (Jest + Supertest)
    const request = require('supertest');
    const app = require('../app');
    const userService = require('../services/userService');
    
    // 서비스 레이어 모킹
    jest.mock('../services/userService');
    
    describe('User Controller', () => {
      test('GET /api/users/:id 성공 시 200 상태 코드와 사용자 데이터 반환', async () => {
        // 서비스 응답 모킹
        userService.getUserById.mockResolvedValue({
          id: 1,
          username: 'testuser',
          email: 'test@example.com'
        });
    
        // API 요청 실행
        const response = await request(app).get('/api/users/1');
    
        // 응답 검증
        expect(response.status).toBe(200);
        expect(response.body).toHaveProperty('id', 1);
        expect(response.body).toHaveProperty('username', 'testuser');
      });
    
      test('GET /api/users/:id 사용자가 없을 경우 404 반환', async () => {
        // 사용자를 찾을 수 없는 경우 모킹
        userService.getUserById.mockResolvedValue(null);
    
        // API 요청 실행
        const response = await request(app).get('/api/users/999');
    
        // 응답 검증
        expect(response.status).toBe(404);
      });
    });
    
  2. 서비스 레이어 테스팅
    비즈니스 로직을 담당하는 서비스 함수를 테스트한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    # Python의 pytest를 사용한 서비스 레이어 테스트 예시
    import pytest
    from unittest.mock import MagicMock
    from services.user_service import UserService
    from repositories.user_repository import UserRepository
    
    def test_get_user_by_id():
        # 리포지토리 모킹
        mock_repo = MagicMock(spec=UserRepository)
        mock_repo.find_by_id.return_value = {
            "id": 1,
            "username": "testuser",
            "email": "test@example.com"
        }
    
        # 서비스 인스턴스 생성
        user_service = UserService(repository=mock_repo)
    
        # 함수 실행
        user = user_service.get_user_by_id(1)
    
        # 결과 검증
        assert user["id"] == 1
        assert user["username"] == "testuser"
        assert user["email"] == "test@example.com"
    
        # 리포지토리 호출 검증
        mock_repo.find_by_id.assert_called_once_with(1)
    
  3. 데이터 액세스 레이어 테스팅
    데이터베이스와 상호 작용하는 코드를 테스트한다.
    이 경우 실제 데이터베이스 대신 인메모리 데이터베이스나 모의 객체를 사용하는 것이 좋다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    
    // Jest와 인메모리 MongoDB를 사용한 데이터 액세스 레이어 테스트 예시
    const { MongoMemoryServer } = require('mongodb-memory-server');
    const mongoose = require('mongoose');
    const UserRepository = require('../repositories/userRepository');
    const UserModel = require('../models/userModel');
    
    describe('User Repository', () => {
      let mongoServer;
    
      // 테스트 전 인메모리 MongoDB 설정
      beforeAll(async () => {
        mongoServer = await MongoMemoryServer.create();
        await mongoose.connect(mongoServer.getUri());
      });
    
      // 테스트 후 연결 해제
      afterAll(async () => {
        await mongoose.disconnect();
        await mongoServer.stop();
      });
    
      // 각 테스트 전 데이터베이스 초기화
      beforeEach(async () => {
        await UserModel.deleteMany({});
      });
    
      test('사용자 생성 후 ID로 조회', async () => {
        // 테스트 데이터 준비
        const userData = {
          username: 'testuser',
          email: 'test@example.com',
          password: 'password123'
        };
    
        // 리포지토리 인스턴스 생성
        const userRepo = new UserRepository();
    
        // 사용자 생성
        const createdUser = await userRepo.create(userData);
    
        // ID로 사용자 조회
        const foundUser = await userRepo.findById(createdUser._id);
    
        // 결과 검증
        expect(foundUser).not.toBeNull();
        expect(foundUser.username).toBe(userData.username);
        expect(foundUser.email).toBe(userData.email);
      });
    });
    

GraphQL API 단위 테스팅

GraphQL API의 단위 테스팅은 리졸버 함수, 스키마 검증, 쿼리 실행 등의 측면에서 이루어진다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Jest와 Apollo Server를 사용한 GraphQL 리졸버 테스트 예시
const { ApolloServer } = require('apollo-server');
const { typeDefs } = require('../schema');
const { resolvers } = require('../resolvers');
const UserService = require('../services/userService');

// 서비스 모킹
jest.mock('../services/userService');

describe('User Resolvers', () => {
  let server;
  
  beforeAll(() => {
    server = new ApolloServer({
      typeDefs,
      resolvers
    });
  });
  
  test('user 쿼리가 ID로 사용자를 반환해야 함', async () => {
    // 서비스 응답 모킹
    UserService.getUserById.mockResolvedValue({
      id: '1',
      username: 'testuser',
      email: 'test@example.com'
    });
    
    // 쿼리 실행
    const result = await server.executeOperation({
      query: `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            username
            email
          }
        }
      `,
      variables: { id: '1' }
    });
    
    // 결과 검증
    expect(result.errors).toBeUndefined();
    expect(result.data.user).toEqual({
      id: '1',
      username: 'testuser',
      email: 'test@example.com'
    });
  });
});

API 단위 테스팅 모범 사례

  1. 경량화된 테스트 작성

    • 실제 데이터베이스 연결이나 외부 서비스 호출 피하기
    • 필요할 때만 모킹 사용하기
    • 테스트 설정을 간단하게 유지하기
  2. 테스트 격리 유지

    • 각 테스트는 독립적으로 실행되어야 한다.
    • 공유 상태를 피하고 테스트마다 상태를 초기화한다.
    • 테스트 간 의존성을 제거한다.
  3. 가독성 높은 테스트 코드 작성

    • 명확한 테스트 이름 사용
    • 테스트 목적과 의도를 명확히 표현
    • 복잡한 설정 로직은 헬퍼 함수로 추출
    1
    2
    3
    4
    5
    6
    7
    8
    
    // 좋은 테스트 이름 예시
    test('getUserById는 존재하는 ID로 조회 시 사용자 객체를 반환해야 함', () => {
      // 테스트 내용
    });
    
    test('getUserById는 존재하지 않는 ID로 조회 시 null을 반환해야 함', () => {
      // 테스트 내용
    });
    
  4. 테스트 더블 적절히 활용

    • Stub: 특정 응답을 반환하도록 하드코딩된 구현체
    • Mock: 호출 여부와 방법을 검증할 수 있는 구현체
    • Fake: 실제 구현체를 단순화한 버전 (예: 인메모리 데이터베이스)
    • Spy: 호출을 기록하는 래퍼 함수
  5. TDD(테스트 주도 개발) 고려

    • 실패하는 테스트 작성
    • 테스트를 통과하는 최소한의 코드 작성
    • 코드 리팩토링
    • 반복

API 단위 테스팅 자동화

  1. CI/CD 파이프라인 통합

    • 모든 코드 변경 시 자동으로 테스트 실행
    • 테스트 실패 시 빌드 실패 설정
    • 코드 커버리지 리포트 생성 및 모니터링
  2. 테스트 실행 스크립트
    일관된 테스트 실행을 위한 npm 스크립트 예시:

    1
    2
    3
    4
    5
    6
    7
    
    {
      "scripts": {
        "test": "jest",
        "test:watch": "jest --watch",
        "test:coverage": "jest --coverage"
      }
    }
    
  3. 테스트 보고서 및 문서화

    • JUnit 형식의 XML 보고서 생성
    • HTML 커버리지 리포트 생성
    • 자동화된 문서 생성

실제 API 단위 테스팅 구현 단계

  1. 테스트 요구사항 정의: 테스트해야 할 API 기능과 시나리오 식별
  2. 테스트 환경 설정: 테스트 프레임워크 및 도구 설치
  3. 테스트 케이스 작성: 각 기능에 대한 단위 테스트 케이스 개발
  4. 테스트 실행: 로컬 환경에서 테스트 실행 및 디버깅
  5. 테스트 자동화: CI/CD 파이프라인에 테스트 통합
  6. 테스트 유지 관리: 코드 변경에 따라 테스트 업데이트

용어 정리

용어설명

참고 및 출처