Integration Testing#
API 통합 테스트는 API 개발 및 설계에서 필수적인 단계로, 개별 컴포넌트들이 서로 올바르게 상호작용하는지 검증하는 프로세스이다.
API 통합 테스트의 기본 개념#
통합 테스트의 정의와 중요성#
API 통합 테스트는 개별적으로 개발된 소프트웨어 모듈이나 서비스가 함께 작동할 때 올바르게 기능하는지 검증하는 테스트 방법이다. 단위 테스트가 개별 함수나 메소드의 정확성을 검증한다면, 통합 테스트는 이러한 개별 부분들이 함께 작동할 때 예상대로 동작하는지 확인한다.
API 통합 테스트가 중요한 이유는 다음과 같다:
- 인터페이스 호환성 검증: 서로 다른 컴포넌트 간의 인터페이스가 일관되게 작동하는지 확인한다.
- 데이터 흐름 검증: 시스템 전체를 통과하는 데이터 흐름이 올바른지 검증한다.
- 의존성 문제 발견: 컴포넌트 간 의존성으로 인한 문제를 조기에 발견한다.
- 환경 문제 식별: 네트워크, 데이터베이스 등 실제 환경에서 발생할 수 있는 문제를 식별한다.
- 비기능적 요구사항 검증: 성능, 보안, 신뢰성 등과 같은 비기능적 요구사항을 검증한다.
단위 테스트와 통합 테스트의 차이점#
통합 테스트와 단위 테스트는 목적과 범위에서 명확한 차이가 있다:
특성 | 단위 테스트 | 통합 테스트 |
---|
테스트 대상 | 개별 함수, 메소드, 클래스 | 여러 컴포넌트나 시스템의 상호작용 |
범위 | 좁음 (단일 기능 단위) | 넓음 (여러 기능 단위의 조합) |
복잡성 | 낮음 | 중간~높음 |
의존성 처리 | 모킹, 스텁 등으로 의존성 격리 | 실제 의존성 사용 또는 일부 의존성만 격리 |
실행 속도 | 빠름 | 중간~느림 |
발견하는 문제 유형 | 알고리즘 오류, 로직 오류 | 인터페이스 불일치, 데이터 흐름 오류, 환경 문제 |
작성 주체 | 일반적으로 개발자 | 개발자 및 QA 팀 |
API 통합 테스트의 유형#
API 통합 테스트는 다양한 방식으로 분류할 수 있다:
테스트 범위에 따른 분류
- 컴포넌트 통합 테스트: API 내의 특정 컴포넌트들 간의 상호작용 테스트
- 시스템 통합 테스트: 전체 API 시스템과 외부 의존성(데이터베이스, 외부 서비스 등) 간의 통합 테스트
- 엔드투엔드 통합 테스트: 사용자 관점에서 전체 시스템의 흐름을 테스트
통합 방식에 따른 분류
- 상향식(Bottom-up) 통합: 낮은 레벨의 컴포넌트부터 시작하여 상위 레벨로 통합해 나가는 방식
- 하향식(Top-down) 통합: 상위 레벨 컴포넌트부터 시작하여 하위 레벨로 통합해 나가는 방식
- 샌드위치 통합: 상향식과 하향식을 결합한 방식
- 빅뱅 통합: 모든 컴포넌트를 동시에 통합하여, 테스트하는 방식
API 통합 테스트 계획 및 전략#
- 테스트 범위 정의
효과적인 통합 테스트 계획을 수립하기 위해서는 먼저 테스트 범위를 명확히 정의해야 한다:- 통합 지점 식별: API 컴포넌트 간의 주요 통합 지점 식별
- 중요 흐름 파악: 핵심 비즈니스 프로세스와 데이터 흐름 파악
- 의존성 매핑: 내부 및 외부 의존성 관계 매핑
- 경계 조건 정의: 시스템 경계와 테스트 경계 정의
- 테스트 우선순위 설정: 비즈니스 중요도와 위험도에 따른 우선순위 설정
- 통합 테스트 전략 수립
통합 테스트 전략은 팀과 프로젝트의 특성에 맞게 수립해야 한다:- 통합 접근 방식 선택
프로젝트 특성에 따라 적절한 통합 접근 방식을 선택한다:- 점진적 통합: 컴포넌트를 단계적으로 통합하여, 문제 발생 시 원인 파악이 용이
- 연속적 통합: CI/CD 파이프라인에 통합하여 지속적으로 테스트 실행
- 테스트 환경 전략
- 환경 분리: 개발, 테스트, 스테이징, 프로덕션 환경 분리
- 환경 일관성: 테스트 환경과 프로덕션 환경의 유사성 확보
- 데이터 전략: 테스트 데이터 생성, 관리, 정리 전략 수립
- 의존성 처리 전략
- 실제 의존성 사용: 실제 의존성을 사용하여 현실적인 테스트 수행
- 테스트 더블 활용: 필요에 따라 스텁, 목, 가짜 객체 등을 활용
- 서비스 가상화: 외부 서비스를 가상화하여 통제된 환경에서 테스트
API 통합 테스트 구현 방법#
테스트 케이스 설계#
효과적인 API 통합 테스트 케이스 설계를 위한 접근 방법:
시나리오 기반 테스트 케이스#
실제 사용자 시나리오를 기반으로 테스트 케이스를 설계한다:
- 핵심 비즈니스 프로세스 식별: 주요 비즈니스 흐름 식별
- 엔드투엔드 시나리오 정의: 사용자 관점에서의 전체 흐름 정의
- 단계별 검증 포인트 설정: 각 단계에서 검증해야 할 사항 정의
인터페이스 기반 테스트 케이스#
API 인터페이스를 중심으로 테스트 케이스를 설계한다:
- 계약 검증: API 계약(명세)에 따른 요청/응답 형식 검증
- 의존성 호출 검증: 외부 시스템 호출의 정확성 검증
- 오류 처리 검증: 다양한 오류 상황에서의 API 동작 검증
데이터 흐름 기반 테스트 케이스#
데이터의 흐름과 변환을 중심으로 테스트 케이스를 설계한다:
- 데이터 변환 검증: 데이터 변환 과정의 정확성 검증
- 데이터 일관성 검증: 여러 시스템 간 데이터 일관성 검증
- 데이터 무결성 검증: 트랜잭션 처리와 데이터 무결성 검증
테스트 구현 기법#
HTTP 기반 API 통합 테스트#
REST, GraphQL 등 HTTP 기반 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| // Jest와 Supertest를 사용한 HTTP API 통합 테스트 예제
const request = require('supertest');
const app = require('../app');
describe('주문 처리 API 통합 테스트', () => {
let orderId;
// 통합 흐름: 상품 추가 -> 주문 생성 -> 주문 확인 -> 결제 처리
test('1. 상품을 장바구니에 추가할 수 있어야 함', async () => {
const response = await request(app)
.post('/api/cart/items')
.send({
productId: 'product-123',
quantity: 2
})
.expect(201);
expect(response.body).toHaveProperty('cartId');
expect(response.body.items).toHaveLength(1);
expect(response.body.items[0].productId).toBe('product-123');
});
test('2. 장바구니로부터 주문을 생성할 수 있어야 함', async () => {
const response = await request(app)
.post('/api/orders')
.send({
cartId: 'cart-456',
shippingAddress: {
street: '123 테스트 스트리트',
city: '서울',
zipCode: '12345'
}
})
.expect(201);
orderId = response.body.orderId;
expect(response.body).toHaveProperty('status', 'CREATED');
expect(response.body).toHaveProperty('totalAmount');
});
test('3. 생성된 주문의 상세 정보를 조회할 수 있어야 함', async () => {
const response = await request(app)
.get(`/api/orders/${orderId}`)
.expect(200);
expect(response.body).toHaveProperty('orderId', orderId);
expect(response.body).toHaveProperty('items');
expect(response.body.items).toHaveLength(1);
expect(response.body.status).toBe('CREATED');
});
test('4. 주문에 대한 결제를 처리할 수 있어야 함', async () => {
const response = await request(app)
.post(`/api/orders/${orderId}/payments`)
.send({
paymentMethod: 'CREDIT_CARD',
cardNumber: '4111111111111111',
expiryDate: '12/25',
cvv: '123'
})
.expect(200);
expect(response.body).toHaveProperty('paymentId');
expect(response.body).toHaveProperty('status', 'COMPLETED');
// 주문 상태가 결제 완료로 변경되었는지 확인
const orderResponse = await request(app)
.get(`/api/orders/${orderId}`)
.expect(200);
expect(orderResponse.body.status).toBe('PAID');
});
});
|
데이터베이스 통합 테스트#
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| // Knex.js와 Jest를 사용한 데이터베이스 통합 테스트 예제
const knex = require('../db/knex');
const userService = require('../services/user-service');
describe('사용자 서비스와 데이터베이스 통합 테스트', () => {
beforeAll(async () => {
// 테스트 데이터베이스 초기화
await knex.migrate.latest();
});
afterAll(async () => {
// 테스트 완료 후 연결 종료
await knex.destroy();
});
beforeEach(async () => {
// 테스트 데이터 초기화
await knex('users').truncate();
});
test('사용자 생성 후 조회할 수 있어야 함', async () => {
// 사용자 생성
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'Password123!'
};
const userId = await userService.createUser(userData);
expect(userId).toBeDefined();
// 생성된 사용자 조회
const user = await userService.getUserById(userId);
expect(user).toMatchObject({
id: userId,
username: userData.username,
email: userData.email
});
// 비밀번호는 해시되어 저장되어야 함
expect(user.password).not.toBe(userData.password);
// 데이터베이스에 직접 확인
const dbUser = await knex('users').where({ id: userId }).first();
expect(dbUser).toBeDefined();
expect(dbUser.username).toBe(userData.username);
});
test('이메일로 사용자를 검색할 수 있어야 함', async () => {
// 테스트 사용자들 생성
const users = [
{ username: 'user1', email: 'user1@example.com', password: 'pass1' },
{ username: 'user2', email: 'user2@example.com', password: 'pass2' },
{ username: 'user3', email: 'user3@example.com', password: 'pass3' }
];
// 여러 사용자 생성
for (const user of users) {
await userService.createUser(user);
}
// 이메일로 사용자 검색
const user = await userService.findUserByEmail('user2@example.com');
expect(user).toBeDefined();
expect(user.username).toBe('user2');
// 존재하지 않는 이메일로 검색
const nonExistentUser = await userService.findUserByEmail('nonexistent@example.com');
expect(nonExistentUser).toBeNull();
});
});
|
외부 서비스 통합 테스트#
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| // Nock을 사용한 외부 서비스 통합 테스트 예제
const nock = require('nock');
const paymentService = require('../services/payment-service');
describe('결제 서비스와 외부 결제 게이트웨이 통합 테스트', () => {
afterEach(() => {
nock.cleanAll();
});
test('결제 처리가 성공적으로 이루어져야 함', async () => {
// 외부 결제 게이트웨이 API 모의
nock('https://api.payment-gateway.com')
.post('/v1/charges')
.reply(200, {
id: 'ch_123456',
status: 'succeeded',
amount: 5000,
currency: 'krw'
});
const paymentResult = await paymentService.processPayment({
amount: 5000,
currency: 'krw',
cardToken: 'tok_visa',
description: '테스트 결제'
});
expect(paymentResult).toMatchObject({
transactionId: 'ch_123456',
status: 'completed',
amount: 5000
});
});
test('결제 실패 시 적절한 오류가 반환되어야 함', async () => {
// 외부 결제 게이트웨이 API 모의 (실패 케이스)
nock('https://api.payment-gateway.com')
.post('/v1/charges')
.reply(400, {
error: {
code: 'card_declined',
message: '카드가 거절되었습니다.'
}
});
await expect(paymentService.processPayment({
amount: 5000,
currency: 'krw',
cardToken: 'tok_visa',
description: '테스트 결제'
})).rejects.toThrow('카드가 거절되었습니다.');
});
test('결제 게이트웨이 타임아웃 처리가 되어야 함', async () => {
// 외부 결제 게이트웨이 API 모의 (타임아웃)
nock('https://api.payment-gateway.com')
.post('/v1/charges')
.delayConnection(3000) // 3초 지연
.reply(200, {});
// 타임아웃 설정이 2초인 경우
paymentService.setTimeout(2000);
await expect(paymentService.processPayment({
amount: 5000,
currency: 'krw',
cardToken: 'tok_visa',
description: '테스트 결제'
})).rejects.toThrow('요청 시간이 초과되었습니다.');
});
});
|
API 통합 테스트 도구 및 프레임워크#
HTTP API 테스트 도구#
Postman#
Postman은 API 개발 및 테스트를 위한 가장 인기 있는 도구 중 하나.
주요 기능:
- 컬렉션을 통한 체계적인 API 테스트 관리
- 환경 변수를 활용한 다양한 환경 지원
- 통합 테스트를 위한 테스트 스크립팅
- 자동화된 테스트 실행 및 CI/CD 통합
- Newman을 통한 명령줄 실행
Postman을 활용한 통합 테스트 예시:
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
| // Postman 테스트 스크립트 예제
// Pre-request 스크립트 (사용자 생성)
pm.sendRequest({
url: pm.environment.get("baseUrl") + "/api/users",
method: "POST",
header: {
"Content-Type": "application/json"
},
body: {
mode: "raw",
raw: JSON.stringify({
username: "testuser",
email: "test@example.com",
password: "Password123!"
})
}
}, function (err, res) {
pm.environment.set("userId", res.json().id);
pm.environment.set("authToken", res.json().token);
});
// 테스트 스크립트 (주문 생성 후 검증)
pm.test("상태 코드는 201이어야 함", function () {
pm.response.to.have.status(201);
});
pm.test("주문이 생성되어야 함", function () {
const responseData = pm.response.json();
pm.environment.set("orderId", responseData.id);
pm.expect(responseData).to.have.property('status', 'CREATED');
pm.expect(responseData).to.have.property('items');
pm.expect(responseData.items).to.be.an('array');
});
// 다음 요청을 위한 설정
postman.setNextRequest("Get Order Details");
|
REST-assured#
REST-assured는 Java 기반 REST API 테스트 프레임워크.
주요 기능:
- BDD 스타일 문법
- Java 프로젝트와의 쉬운 통합
- 강력한 응답 검증 기능
- 다양한 인증 방식 지원
REST-assured를 활용한 통합 테스트 예시:
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
51
52
53
54
55
56
| // REST-assured를 활용한 통합 테스트 예제
@Test
public void testOrderProcessFlow() {
// 1. 상품을 장바구니에 추가
String cartId = given()
.contentType(ContentType.JSON)
.body("{ \"productId\": \"p123\", \"quantity\": 2 }")
.when()
.post("/api/cart/items")
.then()
.statusCode(201)
.extract().path("cartId");
// 2. 장바구니로부터 주문 생성
String orderId = given()
.contentType(ContentType.JSON)
.body("{ \"cartId\": \"" + cartId + "\", \"shippingAddress\": { \"street\": \"123 Test St\", \"city\": \"Seoul\", \"zipCode\": \"12345\" } }")
.when()
.post("/api/orders")
.then()
.statusCode(201)
.body("status", equalTo("CREATED"))
.extract().path("orderId");
// 3. 주문 조회
given()
.pathParam("id", orderId)
.when()
.get("/api/orders/{id}")
.then()
.statusCode(200)
.body("orderId", equalTo(orderId))
.body("status", equalTo("CREATED"))
.body("items", hasSize(1))
.body("items[0].productId", equalTo("p123"));
// 4. 결제 처리
given()
.contentType(ContentType.JSON)
.pathParam("id", orderId)
.body("{ \"paymentMethod\": \"CREDIT_CARD\", \"cardNumber\": \"4111111111111111\", \"expiryDate\": \"12/25\", \"cvv\": \"123\" }")
.when()
.post("/api/orders/{id}/payments")
.then()
.statusCode(200)
.body("status", equalTo("COMPLETED"));
// 5. 주문 상태 확인
given()
.pathParam("id", orderId)
.when()
.get("/api/orders/{id}")
.then()
.statusCode(200)
.body("status", equalTo("PAID"));
}
|
테스트 프레임워크#
Jest (JavaScript)#
Jest는 JavaScript 애플리케이션 테스트를 위한 프레임워크.
주요 기능:
- 간편한 설정
- 강력한 모킹 기능
- 비동기 테스트 지원
- 스냅샷 테스팅
JUnit (Java)#
JUnit은 Java 애플리케이션 테스트를 위한 프레임워크.
주요 기능:
- 다양한 어노테이션 지원
- 다양한 어설션 방법 제공
- 테스트 라이프사이클 관리
- 확장 가능한 아키텍처
3. Pytest (Python)#
pytest는 Python 애플리케이션 테스트를 위한 프레임워크.
주요 기능:
- 간결한 테스트 코드
- 강력한 픽스처 시스템
- 풍부한 플러그인 생태계
- 다양한 매개변수화 옵션
통합 테스트 지원 도구#
Docker#
Docker는 일관된 테스트 환경 구성을 위한 컨테이너화 도구.
통합 테스트에서의 활용:
- 격리된 테스트 환경 제공
- 의존성 서비스(DB, 메시지 큐 등) 구성
- 테스트 환경 재현성 보장
Docker-compose를 활용한 통합 테스트 환경 구성 예시:
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
| # docker-compose.yml 예시
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=test
- DB_HOST=db
- REDIS_HOST=cache
depends_on:
- db
- cache
db:
image: postgres:13
environment:
- POSTGRES_USER=test
- POSTGRES_PASSWORD=test
- POSTGRES_DB=testdb
ports:
- "5432:5432"
cache:
image: redis:6
ports:
- "6379:6379"
test:
build:
context: .
dockerfile: Dockerfile.test
environment:
- NODE_ENV=test
- API_URL=http://app:3000
depends_on:
- app
command: npm run test:integration
|
WireMock#
WireMock은 HTTP 기반 API의 모의(mocking)를 위한 도구.
통합 테스트에서의 활용:
- 외부 API 의존성 모의
- 다양한 시나리오와 응답 시뮬레이션
- 상태 기반 응답 구성
WireMock을 활용한 통합 테스트 예시:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Java와 WireMock을 사용한 예제
@Test
public void testPaymentGatewayIntegration() {
// 외부 결제 게이트웨이 API 모의
stubFor(post(urlEqualTo("/v1/charges"))
.withHeader("Content-Type", equalTo("application/json"))
.withRequestBody(containing("\"amount\":5000"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":\"ch_123456\",\"status\":\"succeeded\",\"amount\":5000}")));
// 결제 요청
PaymentResponse response = paymentService.processPayment(new PaymentRequest(5000, "tok_visa"));
// 응답 검증
assertThat(response.getTransactionId(), equalTo("ch_123456"));
assertThat(response.getStatus(), equalTo("completed"));
// WireMock으로 요청이 예상대로 들어왔는지 검증
verify(postRequestedFor(urlEqualTo("/v1/charges"))
.withHeader("Content-Type", equalTo("application/json"))
.withRequestBody(matchingJsonPath("$.amount", equalTo("5000"))));
}
|
API 통합 테스트의 모범 사례#
통합 테스트 설계 모범 사례#
독립적이고 반복 가능한 테스트 작성#
통합 테스트는 독립적으로 실행 가능하고 반복 실행해도 동일한 결과를 도출해야 한다:
- 테스트 격리: 각 테스트는 다른 테스트에 영향을 주지 않아야 함
- 상태 초기화: 각 테스트 전후로 상태를 초기화하여 일관된 환경 보장
- 멱등성 보장: 여러 번 실행해도 동일한 결과 보장
실제 사용 사례에 초점#
통합 테스트는 실제 사용자 시나리오와 비즈니스 프로세스에 초점을 맞춰야 한다:
- 핵심 흐름 중심: 가장 중요한 비즈니스 흐름에 집중
- 엔드투엔드 시나리오: 실제 사용자 관점에서의 전체 흐름 테스트
- 경계 조건 포함: 주요 예외 상황과 경계 조건 포함
적절한 모킹 전략 사용#
효과적인 통합 테스트를 위해 적절한 모킹 전략을 수립해야 한다:
- 선택적 모킹: 테스트 목적에 따라 일부 컴포넌트만 모킹
- 외부 의존성 처리: 외부 서비스는 가능한 한 모킹하여 테스트 안정성 확보
- 모킹 균형: 너무 많은 모킹은 테스트 가치를 떨어뜨림
테스트 환경 관리 모범 사례#
환경 일관성 유지#
테스트 환경의 일관성은 신뢰할 수 있는 통합 테스트의 기본이다:
- 환경 명세화: Docker, Vagrant 등을 통한 환경 명세화
- 버전 관리: 의존성 버전 명시적 관리
- 구성 분리: 환경별 구성 명확히 분리
테스트 데이터 관리#
효과적인 테스트 데이터 관리는 성공적인 통합 테스트의 열쇠이다:
- 데이터 초기화: 테스트 시작 전 데이터 상태 초기화
- 테스트 데이터 분리: 테스트 간 데이터 격리
- 데이터 정리: 테스트 완료 후 생성된 데이터 정리
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
| // Jest를 사용한 테스트 데이터 초기화 및 정리 예제
describe('주문 서비스 통합 테스트', () => {
beforeAll(async () => {
// 테스트 데이터베이스 연결
await db.connect();
});
afterAll(async () => {
// 데이터베이스 연결 종료
await db.disconnect();
});
beforeEach(async () => {
// 테스트 데이터 초기화
await db.clearAllCollections();
await seedTestData();
});
afterEach(async () => {
// 테스트 중 생성된 임시 파일 정리
await cleanupTempFiles();
});
// 각 테스트 케이스…
});
|
테스트 속도 최적화#
통합 테스트의 실행 속도를 최적화하는 것은 중요하다:
- 병렬 실행: 독립적인 테스트의 병렬 실행
- 리소스 공유: 가능한 경우 설정 비용이 높은 리소스 공유
- 테스트 범위 최적화: 불필요한 단계 제거
테스트 유지 관리 모범 사례#
코드 품질 유지#
통합 테스트 코드도 프로덕션 코드와 같은 수준의 품질을 유지해야 한다:
- 코드 리뷰: 테스트 코드에 대한 정기적 리뷰
- 리팩토링: 중복 코드 제거 및 테스트 코드 리팩토링
- 문서화: 복잡한 테스트 설정 및 목적 문서화
실패 분석 및 디버깅#
통합 테스트 실패에 대한 효과적인 분석과 디버깅 방법을 갖추어야 한다:
- 상세 로깅: 테스트 실패 시 충분한 정보 로깅
- 단계별 검증: 복잡한 테스트의 경우 중간 단계 검증
- 격리된 재현: 실패한 테스트를 격리하여 재현 가능하도록 구성
API 통합 테스트의 도전 과제와 해결책#
비결정적 테스트 문제#
통합 테스트에서 흔히 발생하는 비결정적(flaky) 테스트 문제의 해결책:
도전 과제#
- 동일한 테스트가 때로는 성공하고 때로는 실패하는 현상
- 타이밍, 병렬 실행, 외부 의존성 등으로 인한 불안정성
- 테스트 신뢰성 저하 및 개발자 좌절감 증가
해결책#
- 타임아웃 및 재시도 메커니즘: 적절한 타임아웃 설정 및 재시도 로직 구현
- 테스트 격리: 테스트 간 격리 강화
- 비동기 처리 개선: 비동기 작업에 대한 적절한 대기 전략 구현
- 의존성 모킹: 불안정한 외부 의존성 모킹
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
| // 비동기 작업에 대한 안정적인 대기 구현 예제
async function waitForCondition(conditionFn, options = {}) {
const { timeout = 5000, interval = 100, errorMessage = '조건이 충족되지 않았습니다' } = options;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await conditionFn()) {
return true;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error(errorMessage);
}
// 사용 예시
test('주문 상태가 결제 완료로 변경되어야 함', async () => {
// 결제 요청
await paymentService.processPayment(orderId, paymentDetails);
// 주문 상태 변경 대기
await waitForCondition(
async () => {
const order = await orderService.getOrder(orderId);
return order.status === 'PAID';
},
{ timeout: 3000, errorMessage: '주문 상태가 결제 완료로 변경되지 않았습니다' }
);
// 추가 검증
const updatedOrder = await orderService.getOrder(orderId);
expect(updatedOrder.paymentDetails).toBeDefined();
});
|
의존성 관리 문제#
통합 테스트에서의 복잡한 의존성 관리 문제와 해결책:
도전 과제#
- 다수의 내부 및 외부 의존성
- 의존성으로 인한 테스트 설정 복잡성
- 의존성 변경에 따른 테스트 취약성
해결책#
- 의존성 주입: 명시적 의존성 주입을 통한 제어 가능성 확보
- 서비스 가상화: 가상 서비스를 통한 외부 의존성 시뮬레이션
- 컨테이너 기술: Docker와 같은 컨테이너 기술을 활용한 의존성 격리
- 테스트 더블 계층화: 목적에 맞는 다양한 수준의 테스트 더블 활용
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
| // Spring Boot와 TestContainers를 활용한 의존성 관리 예제
@SpringBootTest
@Testcontainers
public class OrderServiceIntegrationTest {
@Container
private static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Container
private static GenericContainer<?> redis = new GenericContainer<>("redis:6")
.withExposedPorts(6379);
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", redis::getFirstMappedPort);
}
@MockBean
private PaymentGatewayClient paymentGatewayClient;
@Autowired
private OrderService orderService;
@BeforeEach
void setup() {
// 외부 결제 게이트웨이 모의 응답 설정
when(paymentGatewayClient.processPayment(any()))
.thenReturn(new PaymentResponse("tx_123", "succeeded"));
}
@Test
void testCreateOrderAndPayment() {
// 테스트 로직
}
}
|
테스트 속도 문제#
통합 테스트의 실행 속도 문제와 해결책:
도전 과제#
- 긴 테스트 실행 시간
- CI/CD 파이프라인 지연
- 느린 피드백 루프로 인한 개발 효율성 저하
해결책#
- 병렬 테스트 실행: 독립적인 테스트의 병렬 실행
- 테스트 최적화: 불필요한 단계 제거 및 효율적인 설정
- 선택적 실행: 변경된 부분에 관련된 테스트만 선택적 실행
- 테스트 분류: 속도에 따른 테스트 분류 및 실행 전략 수립
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
| // Jest의 병렬 실행 구성 예제 (jest.config.js)
module.exports = {
// 최대 병렬 작업자 수 설정
maxWorkers: '50%',
// 테스트 타임아웃 설정
testTimeout: 30000,
// 속도에 따른 테스트 분류
projects: [
{
displayName: 'fast',
testMatch: ['**/__tests__/**/*.fast.test.js'],
maxWorkers: 4
},
{
displayName: 'integration',
testMatch: ['**/__tests__/**/*.integration.test.js'],
maxWorkers: 2
}
],
// 테스트 우선순위 설정
testSequencer: './customSequencer.js'
};
|
API 통합 테스트 자동화 및 CI/CD 통합#
지속적 통합(CI) 파이프라인 구성#
통합 테스트를 CI 파이프라인에 효과적으로 통합하는 방법:
테스트 실행 전략#
- 빌드마다 실행: 모든 빌드에서 핵심 통합 테스트 실행
- 정기적 실행: 전체 통합 테스트 스위트를 정기적으로 실행
- 병렬 실행: 테스트를 병렬로 실행하여 전체 시간 단축
파이프라인 구성 예시#
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
51
52
53
54
55
56
57
58
59
60
61
| # GitHub Actions 워크플로우 예시
name: API Integration Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
integration-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:6
ports:
- 6379:6379
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run integration tests
run: npm run test:integration
env:
DB_HOST: localhost
DB_PORT: 5432
DB_USER: test
DB_PASSWORD: test
DB_NAME: testdb
REDIS_HOST: localhost
REDIS_PORT: 6379
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: test-results
path: test-results/
|
지속적 배포(CD) 통합#
통합 테스트를 CD 파이프라인에 효과적으로 통합하는 방법:
단계적 배포 전략
- 테스트 환경 배포: 통합 테스트 통과 후 테스트 환경에 배포
- 스테이징 환경 테스트: 스테이징 환경에서 추가 통합 테스트 실행
- 카나리 배포: 일부 사용자에게만 적용하여 실제 환경에서 테스트
롤백 메커니즘
- 자동 롤백: 통합 테스트 실패 시 자동 롤백
- 롤백 검증: 롤백 후 시스템 상태 검증
- 점진적 배포: 문제 발생 시 영향을 최소화하는 점진적 배포
통합 테스트 모니터링 및 보고#
효과적인 통합 테스트 모니터링 및 보고 방법:
테스트 결과 시각화
- 대시보드: 테스트 결과 대시보드 구축
- 트렌드 분석: 시간에 따른 테스트 성공률 및 실행 시간 추세 분석
- 알림 시스템: 테스트 실패 시 알림 시스템 구축
테스트 보고서 생성
- 자세한 보고서: 실패한 테스트에 대한 상세 정보 제공
- 증거 수집: 스크린샷, 로그 등 디버깅에 필요한 증거 수집
- 문제 분류: 실패 원인에 따른 문제 분류
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
| // Jest와 jest-html-reporter를 사용한 보고서 구성 예제
// jest.config.js
module.exports = {
reporters: [
'default',
[
'jest-html-reporter',
{
pageTitle: 'API 통합 테스트 보고서',
outputPath: './test-report.html',
includeFailureMsg: true,
includeSuiteFailure: true,
includeConsoleLog: true,
sort: 'status'
}
],
[
'jest-junit',
{
outputDirectory: './test-results',
outputName: 'junit.xml'
}
]
]
};
|
용어 정리#
참고 및 출처#