API 스키마 기반 테스트 자동 생성, 계약 테스트(Contract Testing), testcontainers와 AI를 결합한 데이터베이스 통합 테스트, 그리고 CI 파이프라인 통합 방법을 다룹니다.
Integration Test(통합 테스트)는 개별 컴포넌트가 올바르게 협력하는지를 검증합니다. 단위 테스트와 E2E 테스트 사이에 위치하며, 비용 대비 가장 높은 결함 검출률을 보이는 구간입니다.
AI가 통합 테스트에 기여하는 영역은 크게 세 가지입니다.
OpenAPI(Swagger) 스키마는 API의 계약을 명확하게 정의합니다. AI는 이 스키마를 분석하여 모든 엔드포인트에 대한 테스트를 자동으로 생성할 수 있습니다.
paths:
/api/users:
post:
summary: 사용자 생성
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, name, password]
properties:
email:
type: string
format: email
name:
type: string
minLength: 2
maxLength: 50
password:
type: string
minLength: 8
responses:
"201":
description: 생성 성공
"400":
description: 유효성 검증 실패
"409":
description: 이메일 중복이 스키마로부터 AI가 도출하는 테스트 시나리오는 다음과 같습니다.
import request from "supertest";
import { app } from "../app";
describe("POST /api/users", () => {
describe("정상 요청", () => {
it("유효한 데이터로 사용자를 생성합니다", async () => {
const response = await request(app)
.post("/api/users")
.send({
email: "test@example.com",
name: "홍길동",
password: "securePass123!",
});
expect(response.status).toBe(201);
expect(response.body).toHaveProperty("id");
expect(response.body.email).toBe("test@example.com");
});
});
describe("유효성 검증", () => {
it("이메일 형식이 올바르지 않으면 400을 반환합니다", async () => {
const response = await request(app)
.post("/api/users")
.send({
email: "invalid-email",
name: "홍길동",
password: "securePass123!",
});
expect(response.status).toBe(400);
});
it("이름이 2자 미만이면 400을 반환합니다", async () => {
const response = await request(app)
.post("/api/users")
.send({
email: "test@example.com",
name: "홍",
password: "securePass123!",
});
expect(response.status).toBe(400);
});
it("비밀번호가 8자 미만이면 400을 반환합니다", async () => {
const response = await request(app)
.post("/api/users")
.send({
email: "test@example.com",
name: "홍길동",
password: "short",
});
expect(response.status).toBe(400);
});
it("필수 필드가 누락되면 400을 반환합니다", async () => {
const response = await request(app)
.post("/api/users")
.send({ email: "test@example.com" });
expect(response.status).toBe(400);
});
});
describe("비즈니스 규칙", () => {
it("중복 이메일로 생성 시 409를 반환합니다", async () => {
// 첫 번째 사용자 생성
await request(app)
.post("/api/users")
.send({
email: "duplicate@example.com",
name: "사용자1",
password: "securePass123!",
});
// 동일 이메일로 두 번째 생성 시도
const response = await request(app)
.post("/api/users")
.send({
email: "duplicate@example.com",
name: "사용자2",
password: "securePass456!",
});
expect(response.status).toBe(409);
});
});
});AI는 스키마의 required, format, minLength, maxLength 등의 제약조건을 분석하여 경계값 테스트를 자동으로 생성합니다. 하지만 "중복 이메일" 같은 비즈니스 규칙은 스키마만으로는 파악이 어려우므로, 응답 코드의 의미를 기반으로 추론합니다.
GraphQL 환경에서도 동일한 접근이 가능합니다. 스키마의 타입 정의와 리졸버 구조를 분석하여 쿼리/뮤테이션 테스트를 생성합니다.
type Query {
user(id: ID!): User
users(page: Int = 1, limit: Int = 20): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
input CreateUserInput {
email: String!
name: String!
password: String!
}import { createTestClient } from "apollo-server-testing";
import { server } from "../graphql-server";
const { query, mutate } = createTestClient(server);
describe("User GraphQL API", () => {
it("ID로 사용자를 조회합니다", async () => {
const GET_USER = `
query GetUser($id: ID!) {
user(id: $id) {
id
email
name
}
}
`;
const result = await query({
query: GET_USER,
variables: { id: "user-1" },
});
expect(result.errors).toBeUndefined();
expect(result.data?.user).toHaveProperty("email");
});
it("페이지네이션으로 사용자 목록을 조회합니다", async () => {
const GET_USERS = `
query GetUsers($page: Int, $limit: Int) {
users(page: $page, limit: $limit) {
edges { node { id email } }
pageInfo { hasNextPage totalCount }
}
}
`;
const result = await query({
query: GET_USERS,
variables: { page: 1, limit: 10 },
});
expect(result.errors).toBeUndefined();
expect(result.data?.users.edges).toBeInstanceOf(Array);
});
});통합 테스트에서 외부 의존성(결제 API, 이메일 서비스, 제3자 API 등)을 실제로 호출하면 테스트가 느려지고 비결정적이 됩니다. AI는 이러한 외부 의존성의 모킹 코드를 자동으로 생성할 수 있습니다.
import { PaymentService } from "./payment-service";
import { PaymentGateway } from "./payment-gateway";
// AI가 PaymentGateway의 인터페이스를 분석하여 자동 생성한 모킹
jest.mock("./payment-gateway");
const mockGateway = jest.mocked(PaymentGateway);
describe("PaymentService", () => {
let service: PaymentService;
beforeEach(() => {
service = new PaymentService(new PaymentGateway());
});
it("결제 성공 시 주문 상태를 완료로 변경합니다", async () => {
mockGateway.prototype.charge.mockResolvedValue({
transactionId: "txn-123",
status: "succeeded",
amount: 50000,
});
const result = await service.processPayment("order-1", 50000);
expect(result.status).toBe("completed");
expect(result.transactionId).toBe("txn-123");
});
it("결제 실패 시 적절한 에러를 반환합니다", async () => {
mockGateway.prototype.charge.mockRejectedValue(
new Error("카드 한도 초과")
);
await expect(service.processPayment("order-1", 5000000))
.rejects.toThrow("카드 한도 초과");
});
it("타임아웃 시 재시도 후 실패합니다", async () => {
mockGateway.prototype.charge
.mockRejectedValueOnce(new Error("TIMEOUT"))
.mockRejectedValueOnce(new Error("TIMEOUT"))
.mockRejectedValueOnce(new Error("TIMEOUT"));
await expect(service.processPayment("order-1", 50000))
.rejects.toThrow("결제 처리 시간이 초과되었습니다");
expect(mockGateway.prototype.charge).toHaveBeenCalledTimes(3);
});
});AI 기반 모킹의 장점은 단순히 모킹 코드를 생성하는 것을 넘어, 실제 서비스의 응답 패턴(성공, 실패, 타임아웃, 네트워크 장애)을 포괄적으로 시뮬레이션한다는 점입니다. 이는 개발자가 놓치기 쉬운 장애 시나리오를 자동으로 테스트에 포함시킵니다.
마이크로서비스 아키텍처에서 서비스 간 인터페이스의 호환성을 보장하는 것은 핵심 과제입니다. Contract Testing(계약 테스트)은 소비자(Consumer)와 제공자(Provider) 간의 약속을 코드로 정의하고 검증하는 방법입니다.
AI는 기존 API 호출 패턴을 분석하여 계약을 자동으로 생성할 수 있습니다.
import { PactV4, MatchersV3 } from "@pact-foundation/pact";
const { like, eachLike, string, integer } = MatchersV3;
const provider = new PactV4({
consumer: "OrderService",
provider: "UserService",
});
describe("UserService 계약", () => {
it("활성 사용자 조회 계약", async () => {
await provider
.addInteraction()
.given("활성 사용자가 존재하는 상태")
.uponReceiving("사용자 조회 요청")
.withRequest("GET", "/api/users/user-1")
.willRespondWith(200, (builder) => {
builder.jsonBody({
id: string("user-1"),
email: string("user@example.com"),
name: string("홍길동"),
isActive: like(true),
createdAt: string("2026-01-01T00:00:00Z"),
});
})
.executeTest(async (mockServer) => {
const client = new UserClient(mockServer.url);
const user = await client.getUser("user-1");
expect(user.id).toBe("user-1");
expect(user.isActive).toBe(true);
});
});
});| 기여 영역 | 설명 |
|---|---|
| 계약 자동 도출 | 프로덕션 트래픽 분석으로 실제 사용 패턴 기반 계약 생성 |
| 호환성 분석 | 스키마 변경 시 영향받는 소비자를 자동 파악 |
| 누락 계약 감지 | 계약이 없는 서비스 간 통신을 찾아 경고 |
| 계약 업데이트 | API 변경 시 계약 파일 자동 업데이트 제안 |
Testcontainers는 Docker 컨테이너를 활용하여 실제 데이터베이스, 메시지 큐, 캐시 등을 테스트 환경에서 구동하는 라이브러리입니다. AI와 결합하면 DB 스키마를 분석하여 의미 있는 통합 테스트를 자동 생성할 수 있습니다.
import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import { Pool } from "pg";
import { UserRepository } from "./user-repository";
describe("UserRepository 통합 테스트", () => {
let container: StartedPostgreSqlContainer;
let pool: Pool;
let repository: UserRepository;
beforeAll(async () => {
container = await new PostgreSqlContainer("postgres:16")
.withDatabase("testdb")
.start();
pool = new Pool({
connectionString: container.getConnectionUri(),
});
// 스키마 마이그레이션
await pool.query(`
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
)
`);
repository = new UserRepository(pool);
}, 60000);
afterAll(async () => {
await pool.end();
await container.stop();
});
afterEach(async () => {
await pool.query("DELETE FROM users");
});
it("사용자를 생성하고 조회합니다", async () => {
const user = await repository.create({
email: "test@example.com",
name: "테스트 사용자",
});
const found = await repository.findById(user.id);
expect(found).not.toBeNull();
expect(found?.email).toBe("test@example.com");
});
it("중복 이메일 생성 시 에러를 반환합니다", async () => {
await repository.create({
email: "duplicate@example.com",
name: "사용자1",
});
await expect(
repository.create({
email: "duplicate@example.com",
name: "사용자2",
})
).rejects.toThrow();
});
it("비활성 사용자를 필터링하여 조회합니다", async () => {
await repository.create({ email: "active@example.com", name: "활성" });
const inactive = await repository.create({
email: "inactive@example.com",
name: "비활성",
});
await repository.deactivate(inactive.id);
const activeUsers = await repository.findActive();
expect(activeUsers).toHaveLength(1);
expect(activeUsers[0].email).toBe("active@example.com");
});
});testcontainers 기반 테스트는 Docker가 필요하며, 컨테이너 시작에 시간이 걸립니다. CI 환경에서는 컨테이너 이미지를 캐싱하고, 여러 테스트가 하나의 컨테이너를 공유하도록 설계해야 합니다. beforeAll에서 컨테이너를 시작하고 afterEach에서 데이터만 정리하는 패턴이 효과적입니다.
통합 테스트는 단위 테스트보다 실행 시간이 길므로, CI 파이프라인에서의 실행 전략이 중요합니다.
name: Integration Tests
on:
pull_request:
branches: [main]
jobs:
integration-tests:
runs-on: ubuntu-latest
strategy:
matrix:
test-suite: [api, database, external]
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run integration tests
run: pnpm test:integration --shard=${{ matrix.test-suite }}
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdbAI를 활용하면 코드 변경 사항을 분석하여 실행이 필요한 통합 테스트만 선택적으로 실행할 수 있습니다. 이는 8장에서 더 깊이 다룹니다.
코드 변경: src/services/payment.ts
-> 영향 분석: PaymentService, OrderService
-> 필요한 테스트: payment.integration.test.ts, order-payment.integration.test.ts
-> 건너뛸 테스트: user.integration.test.ts, notification.integration.test.ts이 장에서는 AI를 활용한 통합 테스트와 API 테스트 자동화를 살펴보았습니다.
핵심 내용을 정리하면 다음과 같습니다.
4장에서는 E2E 테스트로 범위를 확장합니다. 자연어로 테스트를 정의하는 Momentic, testRigor 같은 도구와, DOM 변경에 자동으로 적응하는 셀프 힐링 기능, 그리고 Playwright와 AI를 결합한 실전 E2E 테스트 자동화를 다룹니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
자연어를 E2E 테스트로 변환하는 Momentic, testRigor, Functionize와 DOM 변경에 자동 적응하는 셀프 힐링 기능, Playwright와 AI를 결합한 실전 E2E 테스트 자동화를 다룹니다.
LLM 기반 단위 테스트 자동 생성의 원리와 실전 활용법을 다룹니다. Diffblue, Codium/Qodo 도구를 활용한 pytest/Jest 테스트 생성 실습과 생성된 테스트의 품질 검증 방법을 안내합니다.
변이 테스트의 원리와 변이 연산자를 이해하고, Stryker, PIT, mutmut 도구로 AI 생성 테스트의 품질을 검증하는 방법을 다룹니다. 변이 점수 측정과 비용-효과 분석도 포함합니다.