Integration Testing

API 통합 테스트는 API 개발 및 설계에서 필수적인 단계로, 개별 컴포넌트들이 서로 올바르게 상호작용하는지 검증하는 프로세스이다.

API 통합 테스트의 기본 개념

통합 테스트의 정의와 중요성

API 통합 테스트는 개별적으로 개발된 소프트웨어 모듈이나 서비스가 함께 작동할 때 올바르게 기능하는지 검증하는 테스트 방법이다. 단위 테스트가 개별 함수나 메소드의 정확성을 검증한다면, 통합 테스트는 이러한 개별 부분들이 함께 작동할 때 예상대로 동작하는지 확인한다.

API 통합 테스트가 중요한 이유는 다음과 같다:

  1. 인터페이스 호환성 검증: 서로 다른 컴포넌트 간의 인터페이스가 일관되게 작동하는지 확인한다.
  2. 데이터 흐름 검증: 시스템 전체를 통과하는 데이터 흐름이 올바른지 검증한다.
  3. 의존성 문제 발견: 컴포넌트 간 의존성으로 인한 문제를 조기에 발견한다.
  4. 환경 문제 식별: 네트워크, 데이터베이스 등 실제 환경에서 발생할 수 있는 문제를 식별한다.
  5. 비기능적 요구사항 검증: 성능, 보안, 신뢰성 등과 같은 비기능적 요구사항을 검증한다.

단위 테스트와 통합 테스트의 차이점

통합 테스트와 단위 테스트는 목적과 범위에서 명확한 차이가 있다:

특성단위 테스트통합 테스트
테스트 대상개별 함수, 메소드, 클래스여러 컴포넌트나 시스템의 상호작용
범위좁음 (단일 기능 단위)넓음 (여러 기능 단위의 조합)
복잡성낮음중간~높음
의존성 처리모킹, 스텁 등으로 의존성 격리실제 의존성 사용 또는 일부 의존성만 격리
실행 속도빠름중간~느림
발견하는 문제 유형알고리즘 오류, 로직 오류인터페이스 불일치, 데이터 흐름 오류, 환경 문제
작성 주체일반적으로 개발자개발자 및 QA 팀

API 통합 테스트의 유형

API 통합 테스트는 다양한 방식으로 분류할 수 있다:

  1. 테스트 범위에 따른 분류

    • 컴포넌트 통합 테스트: API 내의 특정 컴포넌트들 간의 상호작용 테스트
    • 시스템 통합 테스트: 전체 API 시스템과 외부 의존성(데이터베이스, 외부 서비스 등) 간의 통합 테스트
    • 엔드투엔드 통합 테스트: 사용자 관점에서 전체 시스템의 흐름을 테스트
  2. 통합 방식에 따른 분류

    • 상향식(Bottom-up) 통합: 낮은 레벨의 컴포넌트부터 시작하여 상위 레벨로 통합해 나가는 방식
    • 하향식(Top-down) 통합: 상위 레벨 컴포넌트부터 시작하여 하위 레벨로 통합해 나가는 방식
    • 샌드위치 통합: 상향식과 하향식을 결합한 방식
    • 빅뱅 통합: 모든 컴포넌트를 동시에 통합하여, 테스트하는 방식

API 통합 테스트 계획 및 전략

  1. 테스트 범위 정의
    효과적인 통합 테스트 계획을 수립하기 위해서는 먼저 테스트 범위를 명확히 정의해야 한다:
    1. 통합 지점 식별: API 컴포넌트 간의 주요 통합 지점 식별
    2. 중요 흐름 파악: 핵심 비즈니스 프로세스와 데이터 흐름 파악
    3. 의존성 매핑: 내부 및 외부 의존성 관계 매핑
    4. 경계 조건 정의: 시스템 경계와 테스트 경계 정의
    5. 테스트 우선순위 설정: 비즈니스 중요도와 위험도에 따른 우선순위 설정
  2. 통합 테스트 전략 수립
    통합 테스트 전략은 팀과 프로젝트의 특성에 맞게 수립해야 한다:
    1. 통합 접근 방식 선택
      프로젝트 특성에 따라 적절한 통합 접근 방식을 선택한다:
      • 점진적 통합: 컴포넌트를 단계적으로 통합하여, 문제 발생 시 원인 파악이 용이
      • 연속적 통합: CI/CD 파이프라인에 통합하여 지속적으로 테스트 실행
    2. 테스트 환경 전략
      • 환경 분리: 개발, 테스트, 스테이징, 프로덕션 환경 분리
      • 환경 일관성: 테스트 환경과 프로덕션 환경의 유사성 확보
      • 데이터 전략: 테스트 데이터 생성, 관리, 정리 전략 수립
    3. 의존성 처리 전략
      • 실제 의존성 사용: 실제 의존성을 사용하여 현실적인 테스트 수행
      • 테스트 더블 활용: 필요에 따라 스텁, 목, 가짜 객체 등을 활용
      • 서비스 가상화: 외부 서비스를 가상화하여 통제된 환경에서 테스트

API 통합 테스트 구현 방법

테스트 케이스 설계

효과적인 API 통합 테스트 케이스 설계를 위한 접근 방법:

시나리오 기반 테스트 케이스

실제 사용자 시나리오를 기반으로 테스트 케이스를 설계한다:

  1. 핵심 비즈니스 프로세스 식별: 주요 비즈니스 흐름 식별
  2. 엔드투엔드 시나리오 정의: 사용자 관점에서의 전체 흐름 정의
  3. 단계별 검증 포인트 설정: 각 단계에서 검증해야 할 사항 정의
인터페이스 기반 테스트 케이스

API 인터페이스를 중심으로 테스트 케이스를 설계한다:

  1. 계약 검증: API 계약(명세)에 따른 요청/응답 형식 검증
  2. 의존성 호출 검증: 외부 시스템 호출의 정확성 검증
  3. 오류 처리 검증: 다양한 오류 상황에서의 API 동작 검증
데이터 흐름 기반 테스트 케이스

데이터의 흐름과 변환을 중심으로 테스트 케이스를 설계한다:

  1. 데이터 변환 검증: 데이터 변환 과정의 정확성 검증
  2. 데이터 일관성 검증: 여러 시스템 간 데이터 일관성 검증
  3. 데이터 무결성 검증: 트랜잭션 처리와 데이터 무결성 검증

테스트 구현 기법

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 개발 및 테스트를 위한 가장 인기 있는 도구 중 하나.

주요 기능:

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 테스트 프레임워크.

주요 기능:

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는 일관된 테스트 환경 구성을 위한 컨테이너화 도구.

통합 테스트에서의 활용:

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)를 위한 도구.

통합 테스트에서의 활용:

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 통합 테스트의 모범 사례

통합 테스트 설계 모범 사례

독립적이고 반복 가능한 테스트 작성

통합 테스트는 독립적으로 실행 가능하고 반복 실행해도 동일한 결과를 도출해야 한다:

실제 사용 사례에 초점

통합 테스트는 실제 사용자 시나리오와 비즈니스 프로세스에 초점을 맞춰야 한다:

적절한 모킹 전략 사용

효과적인 통합 테스트를 위해 적절한 모킹 전략을 수립해야 한다:

테스트 환경 관리 모범 사례

환경 일관성 유지

테스트 환경의 일관성은 신뢰할 수 있는 통합 테스트의 기본이다:

테스트 데이터 관리

효과적인 테스트 데이터 관리는 성공적인 통합 테스트의 열쇠이다:

 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();
});

의존성 관리 문제

통합 테스트에서의 복잡한 의존성 관리 문제와 해결책:

도전 과제
해결책
 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() {
    // 테스트 로직
  }
}

테스트 속도 문제

통합 테스트의 실행 속도 문제와 해결책:

도전 과제
해결책
 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. 롤백 메커니즘

    • 자동 롤백: 통합 테스트 실패 시 자동 롤백
    • 롤백 검증: 롤백 후 시스템 상태 검증
    • 점진적 배포: 문제 발생 시 영향을 최소화하는 점진적 배포

통합 테스트 모니터링 및 보고

효과적인 통합 테스트 모니터링 및 보고 방법:

  1. 테스트 결과 시각화

    • 대시보드: 테스트 결과 대시보드 구축
    • 트렌드 분석: 시간에 따른 테스트 성공률 및 실행 시간 추세 분석
    • 알림 시스템: 테스트 실패 시 알림 시스템 구축
  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
    
    // 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'
          }
        ]
      ]
    };
    

용어 정리

용어설명

참고 및 출처