Unit Testing
단위 테스팅은 API 설계 과정에서 개별 컴포넌트나 함수를 독립적으로 테스트하여 각 부분이 올바르게 작동하는지 확인하는 과정이다. 일반적으로 개발 단계에서 수행되며, 각 컴포넌트를 격리시켜 정확한 작동을 검증함으로써 API 전체의 안정성에 대한 신뢰도를 높이는 것이 주요 목표이다.
단위 테스팅이 중요한 이유:
- 코드 품질 향상: 버그를 조기에 발견하고 수정할 수 있다.
- 리팩토링 지원: 코드 변경 시 기존 기능이 손상되지 않았는지 확인할 수 있다.
- 개발 속도 향상: 문제를 빠르게 발견하여 해결 시간을 단축한다.
- 문서화 역할: 테스트 자체가 코드의 사용 방법과 예상 동작을 보여주는 문서 역할을 한다.
- 통합 테스팅을 위한 기반: 견고한 단위 테스트는 성공적인 통합 테스트의 토대가 된다.
API 단위 테스팅의 기본 원칙
FIRST 원칙
- Fast: 테스트는 빠르게 실행되어야 한다.
- Independent/Isolated: 각 테스트는 독립적이어야 하며 다른 테스트에 의존하지 않아야 한다.
- Repeatable: 테스트는 어떤 환경에서도 동일한 결과를 제공해야 한다.
- Self-validating: 테스트는 스스로 결과를 검증할 수 있어야 한다.
- Timely: 테스트는 프로덕션 코드 구현 전이나 직후에 작성되어야 한다.
단일 책임 원칙 (SRP)
각 테스트는 하나의 기능 또는 메서드만 테스트해야 한다. 이는 테스트가 실패할 경우 문제를 정확히 파악하는 데 도움이 된다.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 단위 테스팅 구성 요소
테스트 대상
- 컨트롤러/라우터 함수
- 서비스 레이어 함수
- 데이터 접근 레이어
- 유틸리티 함수
- 미들웨어
테스트 도구 및 프레임워크
언어 및 프레임워크별 인기 있는 테스트 도구:
JavaScript/Node.js:- Jest
- Mocha + Chai
- Jasmine
Python: - pytest
- unittest
- nose
Java: - JUnit
- TestNG
- Mockito
목(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 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
코드 커버리지
테스트가 코드의 얼마나 많은 부분을 실행하는지 측정한다.
일반적으로 다음과 같은 유형의 커버리지를 고려한다:- 라인 커버리지: 실행된 코드 라인의 비율
- 분기 커버리지: 실행된 코드 브랜치의 비율
- 함수 커버리지: 호출된 함수의 비율
RESTful API 단위 테스팅
컨트롤러 레이어 테스팅
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); }); });
서비스 레이어 테스팅
비즈니스 로직을 담당하는 서비스 함수를 테스트한다.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)
데이터 액세스 레이어 테스팅
데이터베이스와 상호 작용하는 코드를 테스트한다.
이 경우 실제 데이터베이스 대신 인메모리 데이터베이스나 모의 객체를 사용하는 것이 좋다.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의 단위 테스팅은 리졸버 함수, 스키마 검증, 쿼리 실행 등의 측면에서 이루어진다.
|
|
API 단위 테스팅 모범 사례
경량화된 테스트 작성
- 실제 데이터베이스 연결이나 외부 서비스 호출 피하기
- 필요할 때만 모킹 사용하기
- 테스트 설정을 간단하게 유지하기
테스트 격리 유지
- 각 테스트는 독립적으로 실행되어야 한다.
- 공유 상태를 피하고 테스트마다 상태를 초기화한다.
- 테스트 간 의존성을 제거한다.
가독성 높은 테스트 코드 작성
- 명확한 테스트 이름 사용
- 테스트 목적과 의도를 명확히 표현
- 복잡한 설정 로직은 헬퍼 함수로 추출
테스트 더블 적절히 활용
- Stub: 특정 응답을 반환하도록 하드코딩된 구현체
- Mock: 호출 여부와 방법을 검증할 수 있는 구현체
- Fake: 실제 구현체를 단순화한 버전 (예: 인메모리 데이터베이스)
- Spy: 호출을 기록하는 래퍼 함수
TDD(테스트 주도 개발) 고려
- 실패하는 테스트 작성
- 테스트를 통과하는 최소한의 코드 작성
- 코드 리팩토링
- 반복
API 단위 테스팅 자동화
CI/CD 파이프라인 통합
- 모든 코드 변경 시 자동으로 테스트 실행
- 테스트 실패 시 빌드 실패 설정
- 코드 커버리지 리포트 생성 및 모니터링
테스트 실행 스크립트
일관된 테스트 실행을 위한 npm 스크립트 예시:테스트 보고서 및 문서화
- JUnit 형식의 XML 보고서 생성
- HTML 커버리지 리포트 생성
- 자동화된 문서 생성
실제 API 단위 테스팅 구현 단계
- 테스트 요구사항 정의: 테스트해야 할 API 기능과 시나리오 식별
- 테스트 환경 설정: 테스트 프레임워크 및 도구 설치
- 테스트 케이스 작성: 각 기능에 대한 단위 테스트 케이스 개발
- 테스트 실행: 로컬 환경에서 테스트 실행 및 디버깅
- 테스트 자동화: CI/CD 파이프라인에 테스트 통합
- 테스트 유지 관리: 코드 변경에 따라 테스트 업데이트
용어 정리
용어 | 설명 |
---|---|