Functional Testing

API 기능 테스트는 API가 의도한 모든 기능을 올바르게 수행하는지 검증하는 과정이다. API 개발 및 유지보수 과정에서 핵심적인 단계로, 다양한 측면에서 API의 기능적 정확성을 보장한다.

API 기능 테스트의 기본 개념

기능 테스트란 무엇인가?

기능 테스트는 시스템이 사용자 관점에서 의도한 기능을 올바르게 수행하는지 검증하는 테스트 방법이다. API 기능 테스트는 특별히 API 엔드포인트가 예상대로 동작하는지 확인하는 데 초점을 맞춘다.

API 기능 테스트의 목적

API 기능 테스트의 주요 목적은 다음과 같다:

다른 API 테스트 유형과의 관계

API 기능 테스트는 다음과 같은 다른 테스트 유형과 함께 종합적인, API 테스트 전략을 구성한다:

API 기능 테스트의 중요 요소

  1. 요청-응답 페어 테스트
    API의 가장 기본적인 기능 검증 방법은 요청과 응답의 쌍을 테스트하는 것이다.

    • HTTP 메소드 검증: GET, POST, PUT, DELETE 등 HTTP 메소드가 올바르게 처리되는지 확인
    • 경로 변수와 쿼리 파라미터: URL 경로 변수와 쿼리 파라미터가 올바르게 처리되는지 검증
    • 요청 본문(Request Body): 요청 본문의 데이터가 올바르게 처리되는지 검증
    • 응답 상태 코드: 다양한 상황에서 적절한 HTTP 상태 코드를 반환하는지 확인
    • 응답 본문(Response Body): 응답 본문이 예상된 형식과 데이터를 포함하는지 검증
  2. 데이터 검증
    API가 처리하는 데이터의 정확성을 검증하는 것은 매우 중요하다.

    • 데이터 타입: 필드가 올바른 데이터 타입으로 반환되는지 검증
    • 필수 필드: 필수 필드가 모두 존재하는지 확인
    • 데이터 범위: 값이 허용된 범위 내에 있는지 검증
    • 데이터 형식: 날짜, 이메일, 전화번호 등의 형식이 올바른지 확인
    • 데이터 관계: 관련 데이터 간의 관계가 올바르게 유지되는지 검증
  3. 오류 처리 및 예외 상황
    API가 다양한 오류와 예외 상황을 올바르게 처리하는지 확인하는 것은 기능 테스트의 중요한 부분이다.

    • 유효하지 않은 입력: 잘못된 형식이나 범위의 입력에 대한 처리 검증
    • 필수 파라미터 누락: 필수 파라미터가 누락된, 경우 적절한 오류 메시지 반환 여부 확인
    • 권한 없는 접근: 권한이 없는 요청에 대한 적절한 오류 응답 검증
    • 서버 오류 상황: 서버 오류 발생 시 적절한 오류 처리 확인
    • 타임아웃 처리: 타임아웃 상황에서의 API 동작 검증
  4. 비즈니스 로직 검증
    API의 핵심은 비즈니스 로직을 올바르게 구현하는 것이다.

    • 비즈니스 규칙: 애플리케이션의 비즈니스 규칙이 올바르게 적용되는지 검증
    • 계산 로직: 금액 계산, 할인, 세금 등의 계산이 정확한지 확인
    • 데이터 변환: 데이터 변환 로직이 올바르게 작동하는지 검증
    • 상태 변경: 리소스 상태 변경이 요구사항에 따라 올바르게 이루어지는지 확인
    • 트랜잭션 처리: 트랜잭션이 올바르게 처리되는지 검증

API 기능 테스트 방법론

블랙박스 테스트 접근법

블랙박스 테스트는 API의 내부 구현을 고려하지 않고 외부 동작만 검증하는 방법이다.

화이트박스 테스트 접근법

화이트박스 테스트는 API의 내부 구현을 고려하여 테스트 케이스를 설계하는 방법이다.

자동화된 API 기능 테스트

현대적인 API 개발 환경에서는 테스트 자동화가 필수적이다.

API 기능 테스트 구현 실전 예제

REST API 기능 테스트 예제

사용자 관리 REST 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
// Jest와 Supertest를 사용한 사용자 생성 API 테스트
const request = require('supertest');
const app = require('../app');

describe('사용자 API 기능 테스트', () => {
  // 유효한 데이터로 사용자 생성 테스트
  test('유효한 데이터로 사용자 생성 시 201 상태코드와 생성된 사용자 정보를 반환해야 함', async () => {
    const userData = {
      username: 'testuser',
      email: 'test@example.com',
      password: 'Password123!'
    };
    
    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .expect('Content-Type', /json/)
      .expect(201);
    
    // 응답 본문 검증
    expect(response.body).toHaveProperty('id');
    expect(response.body.username).toBe(userData.username);
    expect(response.body.email).toBe(userData.email);
    // 비밀번호는 응답에 포함되지 않아야 함
    expect(response.body).not.toHaveProperty('password');
  });
  
  // 이미 존재하는 이메일로 사용자 생성 테스트
  test('이미 존재하는 이메일로 사용자 생성 시 409 상태코드를 반환해야 함', async () => {
    // 먼저 사용자 생성
    const userData = {
      username: 'existinguser',
      email: 'existing@example.com',
      password: 'Password123!'
    };
    
    await request(app)
      .post('/api/users')
      .send(userData);
    
    // 같은 이메일로 다시 생성 시도
    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .expect('Content-Type', /json/)
      .expect(409);
    
    // 오류 메시지 검증
    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toContain('already exists');
  });
  
  // 유효하지 않은 이메일 형식 테스트
  test('유효하지 않은 이메일 형식으로 사용자 생성 시 400 상태코드를 반환해야 함', async () => {
    const userData = {
      username: 'invaliduser',
      email: 'invalid-email',  // 잘못된 이메일 형식
      password: 'Password123!'
    };
    
    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .expect('Content-Type', /json/)
      .expect(400);
    
    // 오류 메시지 검증
    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toContain('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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// Jest와 Apollo Server Testing을 사용한 GraphQL API 테스트
const { createTestClient } = require('apollo-server-testing');
const { ApolloServer, gql } = require('apollo-server');
const { typeDefs, resolvers } = require('../schema');

describe('상품 GraphQL API 기능 테스트', () => {
  let query, mutate;
  
  beforeAll(() => {
    // 테스트용 Apollo 서버 설정
    const server = new ApolloServer({
      typeDefs,
      resolvers,
      context: () => ({ currentUser: { id: 'admin', role: 'ADMIN' } })
    });
    
    const testClient = createTestClient(server);
    query = testClient.query;
    mutate = testClient.mutate;
  });
  
  // 상품 조회 테스트
  test('ID로 상품을 조회할 수 있어야 함', async () => {
    const GET_PRODUCT = gql`
      query GetProduct($id: ID!) {
        product(id: $id) {
          id
          name
          price
          description
          category
        }
      }
    `;
    
    // 기존 상품 ID로 조회
    const response = await query({
      query: GET_PRODUCT,
      variables: { id: 'product-1' }
    });
    
    // 응답 검증
    expect(response.errors).toBeUndefined();
    expect(response.data.product).toBeDefined();
    expect(response.data.product.id).toBe('product-1');
    expect(response.data.product.name).toBeDefined();
    expect(response.data.product.price).toBeGreaterThan(0);
  });
  
  // 상품 생성 테스트
  test('새 상품을 생성할 수 있어야 함', async () => {
    const CREATE_PRODUCT = gql`
      mutation CreateProduct($input: ProductInput!) {
        createProduct(input: $input) {
          id
          name
          price
          description
          category
        }
      }
    `;
    
    const newProduct = {
      name: '새 상품',
      price: 1000,
      description: '새로운 테스트 상품입니다',
      category: 'TEST'
    };
    
    const response = await mutate({
      mutation: CREATE_PRODUCT,
      variables: { input: newProduct }
    });
    
    // 응답 검증
    expect(response.errors).toBeUndefined();
    expect(response.data.createProduct).toBeDefined();
    expect(response.data.createProduct.id).toBeDefined();
    expect(response.data.createProduct.name).toBe(newProduct.name);
    expect(response.data.createProduct.price).toBe(newProduct.price);
  });
});

gRPC API 기능 테스트 예제

gRPC 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
// gRPC API 테스트 예제 (Node.js)
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const { expect } = require('chai');

// Proto 파일 로드
const packageDefinition = protoLoader.loadSync('path/to/inventory.proto');
const inventoryProto = grpc.loadPackageDefinition(packageDefinition).inventory;

describe('재고 gRPC API 기능 테스트', () => {
  let client;
  
  before(() => {
    // gRPC 클라이언트 설정
    client = new inventoryProto.InventoryService(
      'localhost:50051',
      grpc.credentials.createInsecure()
    );
  });
  
  // 상품 재고 조회 테스트
  it('상품 ID로 재고를 조회할 수 있어야 함', (done) => {
    client.getStock({ productId: 'product-1' }, (err, response) => {
      expect(err).to.be.null;
      expect(response).to.have.property('quantity');
      expect(response.quantity).to.be.a('number');
      done();
    });
  });
  
  // 재고 업데이트 테스트
  it('상품 재고를 업데이트할 수 있어야 함', (done) => {
    const newQuantity = 100;
    
    client.updateStock({
      productId: 'product-1',
      quantity: newQuantity
    }, (err, response) => {
      expect(err).to.be.null;
      expect(response).to.have.property('success');
      expect(response.success).to.be.true;
      
      // 업데이트된 재고 확인
      client.getStock({ productId: 'product-1' }, (err, response) => {
        expect(err).to.be.null;
        expect(response.quantity).to.equal(newQuantity);
        done();
      });
    });
  });
  
  // 존재하지 않는 상품 재고 조회 테스트
  it('존재하지 않는 상품 ID로 재고 조회 시 오류를 반환해야 함', (done) => {
    client.getStock({ productId: 'non-existent' }, (err, response) => {
      expect(err).to.not.be.null;
      expect(err.code).to.equal(grpc.status.NOT_FOUND);
      done();
    });
  });
});

API 기능 테스트 계획 및 전략

  1. 테스트 범위 결정
    효과적인 API 기능 테스트를 위해서는 테스트 범위를, 명확히 정의해야 한다.

    • 핵심 기능 식별: API의 핵심 기능을 식별하고 우선순위 지정
    • 엔드포인트 매핑: 모든 API 엔드포인트 목록 작성 및 우선순위 지정
    • 사용 사례(Use Case) 식별: 주요 사용 사례와 시나리오 정의
    • 테스트 레벨 정의: 단위, 통합, 시스템 등 다양한 레벨의 테스트 계획
  2. 테스트 케이스 설계
    API 기능 테스트 케이스는 다양한 시나리오를 포괄해야 한다.

    • 긍정적 테스트 케이스: 유효한 입력으로 API가 올바르게 동작하는지 검증
    • 부정적 테스트 케이스: 유효하지 않은 입력으로 API가 적절하게 오류를 처리하는지 검증
    • 경계값 테스트 케이스: 입력 범위의 경계값에서 API 동작 검증
    • 의존성 테스트 케이스: API 간 의존성이 있는 경우 이를 고려한 테스트 케이스
  3. 테스트 환경 설정
    API 기능 테스트를 위한 적절한 환경 설정이 필요하다.

    • 테스트 데이터베이스: 테스트용 데이터베이스 설정 및 초기 데이터 로드
    • 모의(Mock) 서비스: 외부 서비스 의존성을 가진 경우 모의 서비스 설정
    • 테스트 서버: 테스트용 서버 환경 구성
    • 테스트 도구 설정: Postman, Jest, REST Assured 등 테스트 도구 구성
  4. 테스트 자동화 전략
    효율적인 API 기능 테스트를 위한 자동화 전략이 중요하다.

    • 회귀 테스트 자동화: 기본 기능에 대한 회귀 테스트 자동화
    • 테스트 데이터 관리: 테스트 데이터 생성 및 관리 자동화
    • CI/CD 통합: 지속적 통합 및 배포 파이프라인에 테스트 통합
    • 테스트 보고서 자동화: 테스트 결과 보고서 자동 생성

주요 API 기능 테스트 도구 및 프레임워크

API 테스트 도구

다양한 API 테스트 도구를 활용하여 기능 테스트를 수행할 수 있다.

Postman

Postman은 API 개발 및 테스트를 위한 가장 인기 있는 도구 중 하나.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Postman 테스트 스크립트 예제
pm.test("상태 코드는 200이어야 함", function () {
    pm.response.to.have.status(200);
});

pm.test("응답 시간은 500ms 이하여야 함", function () {
    pm.expect(pm.response.responseTime).to.be.below(500);
});

pm.test("응답에 필수 필드가 있어야 함", function () {
    const responseData = pm.response.json();
    pm.expect(responseData).to.have.property('id');
    pm.expect(responseData).to.have.property('name');
    pm.expect(responseData).to.have.property('price');
});
REST Assured

REST Assured는 Java 기반의 REST API 테스트 도구.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// REST Assured 테스트 예제
@Test
public void testGetUser() {
    given()
        .pathParam("id", 1)
    .when()
        .get("/api/users/{id}")
    .then()
        .statusCode(200)
        .contentType(ContentType.JSON)
        .body("id", equalTo(1))
        .body("name", notNullValue())
        .body("email", matchesPattern("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$"));
}
SoapUI

SoapUI는 SOAP 및 REST API 테스트를 위한 도구.

테스트 프레임워크

다양한 테스트 프레임워크를 활용하여 API 기능 테스트를 구현할 수 있다.

Jest (JavaScript)

Jest는 Facebook에서 개발한 JavaScript 테스트 프레임워크.

JUnit & Spring Test (Java)

JUnit과 Spring Test는 Java 생태계에서 API 테스트에 널리 사용된다.

Pytest (Python)

pytest는 Python 기반 API 테스트에 적합한 프레임워크.

API 기능 테스트의 모범 사례

  1. 테스트 가능한 API 설계
    효과적인 기능 테스트를 위해 API 설계 단계부터 테스트 가능성을 고려해야 한다.

    • 명확한 계약: 명확한 API 계약 및 문서화
    • 독립적인 엔드포인트: 최대한 독립적으로 테스트 가능한 엔드포인트 설계
    • 멱등성: GET, PUT 등의 메소드는 멱등성 보장
    • 상태 관리: 테스트 환경 초기화가 용이하도록 상태 관리
  2. 테스트 데이터 관리
    테스트 데이터 관리는 효과적인 API 기능 테스트를 위해 중요하다.

    • 테스트 데이터 격리: 테스트 간 격리된 데이터 사용
    • 테스트 데이터 생성: 자동화된 테스트 데이터 생성
    • 데이터 정리: 테스트 후 생성된 데이터 정리
    • 데이터 시드: 일관된 테스트를 위한 시드 데이터 활용
  3. 효과적인 테스트 구조화
    테스트 코드의 구조화는 유지보수성과 확장성에 중요하다.

    • 테스트 계층화: 단위, 통합, 기능 테스트 계층 분리
    • 재사용 가능한 코드: 공통 테스트 로직 추출 및 재사용
    • 설정과 테스트 분리: 테스트 설정과 실제 테스트 로직 분리
    • 명확한 테스트 이름: 테스트의 목적을 명확히 설명하는 이름 사용
  4. 연속적인 테스트 실행
    CI/CD 파이프라인에 API 기능 테스트를 통합하는 것이 중요하다.

    • 자동화된 실행: CI/CD 파이프라인에서 자동으로 테스트 실행
    • 빠른 피드백: 개발자에게 빠른 테스트 결과 피드백 제공
    • 테스트 보고서: 테스트 결과를 쉽게 이해할 수 있는 보고서 생성
    • 테스트 모니터링: 테스트 실행 및 결과 모니터링

API 기능 테스트의 도전과 해결책

복잡한 의존성 처리

API는 종종 다른 서비스나 시스템에 의존한다.

도전 과제
해결책

비동기 API 테스트

현대 애플리케이션은 종종 비동기 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
// 비동기 API 테스트 예제 (JavaScript)
test('비동기 주문 처리 테스트', async () => {
  // 주문 생성 요청
  const orderResponse = await request(app)
    .post('/api/orders')
    .send({ productId: 'product-1', quantity: 2 })
    .expect(202); // 비동기 처리를 위한 Accepted 상태코드
  
  const orderId = orderResponse.body.orderId;
  
  // 주문 처리 완료까지 폴링
  let orderStatus = 'PROCESSING';
  let attempts = 0;
  const maxAttempts = 10;
  
  while (orderStatus === 'PROCESSING' && attempts < maxAttempts) {
    await new Promise(resolve => setTimeout(resolve, 500)); // 0.5초 대기
    
    const statusResponse = await request(app)
      .get(`/api/orders/${orderId}/status`)
      .expect(200);
    
    orderStatus = statusResponse.body.status;
    attempts++;
  }
  
  expect(orderStatus).toBe('COMPLETED');
  
  // 완료된 주문 검증
  const orderDetails = await request(app)
    .get(`/api/orders/${orderId}`)
    .expect(200);
  
  expect(orderDetails.body.status).toBe('COMPLETED');
  expect(orderDetails.body.items).toHaveLength(1);
  expect(orderDetails.body.items[0].productId).toBe('product-1');
});

대규모 API 테스트

많은 수의 API 엔드포인트를 테스트해야 하는 경우의 문제.

도전 과제
해결책

API 기능 테스트 측정 및 개선

  1. 테스트 커버리지
    API 기능 테스트의 품질을 측정하기 위한 지표.

    • API 엔드포인트 커버리지: 테스트된 API 엔드포인트 비율
    • 경로 커버리지: 테스트된 API 경로 비율
    • 파라미터 커버리지: 테스트된 API 파라미터 조합 비율
    • 코드 커버리지: 테스트로 실행된 코드 비율
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    // Jest를 사용한 코드 커버리지 설정 (package.json)
    {
      "scripts": {
        "test": "jest",
        "test:coverage": "jest --coverage"
      },
      "jest": {
        "collectCoverageFrom": [
          "src/**/*.js",
          "!src/config/**",
          "!**/node_modules/**"
        ],
        "coverageThreshold": {
          "global": {
            "branches": 80,
            "functions": 80,
            "lines": 85,
            "statements": 85
          }
        }
      }
    }
    
  2. 결함 분석
    발견된 결함을 분석하여 테스트 전략을 개선한다.

    • 결함 패턴 식별: 반복적으로 발생하는 결함 패턴 식별
    • 결함 분류: 심각도 및 유형별 결함 분류
    • 근본 원인 분석: 결함의 근본 원인 분석
    • 테스트 개선: 분석 결과를 기반으로 테스트 개선
  3. 테스트 성숙도 모델
    API 기능 테스트의 성숙도를 평가하고 개선한다.

    • 레벨 1: 기본적 수동 테스트: 기본적인 수동 API 테스트
    • 레벨 2: 자동화된 기본 검증: 기본적인 자동화 테스트 구현
    • 레벨 3: 체계적인 테스트 자동화: 포괄적인 테스트 자동화 및 CI/CD 통합
    • 레벨 4: 고급 테스트 전략: 고급 테스트 전략, 성능, 보안 등 포괄적 테스트
    • 레벨 5: 최적화 및 지속적 개선: 테스트 프로세스의 지속적 개선 및 최적화

API 기능 테스트의 최신 트렌드

계약 기반 테스트(Contract-Based Testing)와의 통합

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
// Swagger 기반 API 테스트 자동 생성 예제 (openapi-validator 사용)
const { expect } = require('chai');
const OpenApiValidator = require('openapi-validator').default;

describe('API 계약 기반 기능 테스트', () => {
  let validator;
  
  before(() => {
    validator = new OpenApiValidator({
      apiSpec: './openapi.yaml'
    });
  });
  
  it('사용자 생성 API가 명세를 준수해야 함', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        username: 'testuser',
        email: 'test@example.com',
        password: 'Password123!'
      });
    
    expect(validator.validateResponse(
      response.statusCode,
      response.body,
      '/api/users',
      'post'
    )).to.be.undefined;
  });
});

인공지능과 기계 학습의 활용

AI와 ML 기술을 API 기능 테스트에 활용하는 추세가 증가하고 있다.

API 메시(API Mesh) 테스트

여러 API를 조합한 API 메시 환경에서의 테스트 방법이 발전하고 있다.

시각적 API 테스트

시각적 인터페이스를 통한 API 테스트 방법이 발전하고 있다.

종합 API 기능 테스트 전략

  1. 테스트 피라미드 적용
    효과적인 API 테스트를 위한 테스트 피라미드 전략이다.

    • 단위 테스트(기반): API 구현 코드의 개별 단위 테스트
    • 통합 테스트(중간): API 컴포넌트 간 상호작용 테스트
    • 기능 테스트(상층): 전체 API 기능을 검증하는 테스트
    • E2E 테스트(최상층): 사용자 관점에서의 전체 시스템 테스트
  2. 단계적 테스트 접근법
    API 기능 테스트를 위한 단계적 접근법.

    1. 기본 동작 테스트: API의 기본 동작 검증
    2. 예외 처리 테스트: 예외 상황 및 오류 처리 검증
    3. 비즈니스 로직 테스트: 복잡한 비즈니스 로직 검증
    4. 성능 및 한계 테스트: API의 성능 및 한계 검증
  3. 개발 생명주기 통합
    API 개발 생명주기 전체에 걸친 기능 테스트 통합 전략.

    • 계획 단계: 테스트 요구사항 및 전략 정의
    • 설계 단계: API 설계와 함께 테스트 케이스 설계
    • 구현 단계: 개발과 병행하여 테스트 구현
    • 테스트 단계: 포괄적인 기능 테스트 수행
    • 배포 단계: 배포 전 최종 검증 테스트
    • 유지보수 단계: 지속적인 회귀 테스트

용어 정리

용어설명

참고 및 출처