| name | testing-expert |
| version | 3.0.0 |
| description | Integration Test E2E 테스트, TestRestTemplate 필수, Test Fixtures 재사용.
MockMvc 금지, @Sql 어노테이션 테스트 데이터 설정.
Gradle testFixtures 플러그인 활용, ArchUnit 의존성 검증.
|
| author | claude-spring-standards |
| created | "2024-11-01T00:00:00.000Z" |
| updated | "2025-12-05T00:00:00.000Z" |
| tags | ["project","testing","integration-test","fixtures","testresttemplate","e2e","archunit"] |
Testing Expert (테스팅 전문가)
목적 (Purpose)
Integration Test + Test Fixtures 전략을 담당하는 전문가입니다.
E2E 테스트는 TestRestTemplate 기반 실제 HTTP 요청으로 수행하고,
테스트 데이터는 Gradle testFixtures + @Sql 어노테이션으로 관리합니다.
활성화 조건
/impl {layer} {feature} 명령 실행 시 (테스트 동시 작성)
/plan 실행 후 테스트 전략 결정 시
- integration test, fixture, testresttemplate, @sql, e2e 키워드 언급 시
산출물 (Output)
| 컴포넌트 | 파일명 패턴 | 위치 |
|---|
| Integration Test | {Feature}IntegrationTest.java | bootstrap/src/test/java/.../ |
| Domain Fixture | {Aggregate}Fixture.java | domain/src/testFixtures/java/.../fixture/domain/ |
| Application Fixture | {Command/Response}Fixture.java | application/src/testFixtures/java/.../fixture/application/ |
| Adapter Fixture | {Request/Entity}Fixture.java | adapter-*/src/testFixtures/java/.../fixture/adapter/ |
| SQL Test Data | {feature}-test-data.sql | src/test/resources/sql/ |
완료 기준 (Acceptance Criteria)
테스트 전략 개요
Integration Test = E2E Test
HTTP Request (TestRestTemplate)
↓
Controller (REST API Layer)
↓
UseCase (Application Layer)
↓
Repository (Persistence Layer)
↓
Real Database (MySQL via TestContainers)
↓
HTTP Response
단위 테스트 vs 통합 테스트
| 항목 | 단위 테스트 | 통합 테스트 |
|---|
| 범위 | 하나의 클래스/메서드 | 전체 레이어 통합 |
| 의존성 | Mock/Stub | 실제 Bean + 실제 DB |
| 속도 | 빠름 (ms) | 느림 (초) |
| 테스트 | @DataJpaTest, @WebMvcTest | @SpringBootTest |
| HTTP | MockMvc (가짜) | TestRestTemplate (실제) |
| DB | H2 (인메모리) | MySQL (실제) |
Flyway vs @Sql 구분 (핵심!)
| 항목 | Flyway | @Sql |
|---|
| 목적 | DB 스키마 버전 관리 | 테스트 데이터 삽입 |
| 역할 | DDL (CREATE TABLE) | DML (INSERT) |
| 실행 시점 | 앱 시작 시 (1번) | 각 테스트 메서드 전 |
| 파일 위치 | db/migration/ | src/test/resources/sql/ |
| 운영 사용 | ✅ 사용 | ❌ 테스트 전용 |
코드 템플릿
1. Integration Test
package com.ryuqq.bootstrap;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Testcontainers
@Transactional
@DisplayName("Order 통합 테스트")
class OrderIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:15-alpine")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test");
@Autowired
private TestRestTemplate restTemplate;
@Test
@Sql("/sql/orders-test-data.sql")
@DisplayName("E2E - 주문 생성")
{
(, , );
ResponseEntity<ApiResponse<OrderResponse>> response = restTemplate.exchange(
,
HttpMethod.POST,
<>(request),
<>() {}
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getData().orderId()).isNotNull();
}
{
;
ResponseEntity<ApiResponse<OrderResponse>> response = restTemplate.exchange(
,
HttpMethod.GET,
,
<>() {},
orderId
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getData().orderId()).isEqualTo(orderId);
}
}
핵심 규칙:
@SpringBootTest(webEnvironment = RANDOM_PORT) 필수
- TestRestTemplate 주입 (MockMvc 금지)
@Sql 로 테스트 데이터 삽입 (DML만)
@Transactional 롤백으로 테스트 격리
@Testcontainers + MySQL 실제 DB
2. Domain Fixture (testFixtures 소스셋)
package com.ryuqq.fixture.domain;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.order.vo.OrderId;
import com.ryuqq.domain.order.vo.OrderStatus;
import com.ryuqq.domain.order.vo.Money;
import java.math.BigDecimal;
public final class OrderFixture {
private OrderFixture() {
throw new AssertionError("Utility class - do not instantiate");
}
public static Order defaultNewOrder() {
return Order.forNew(
new CustomerId(1L),
Money.of(BigDecimal.valueOf(50000))
);
}
public static Order defaultExistingOrder() {
return Order.forExisting(
OrderId.of(1L),
new CustomerId(1L),
Money.of(BigDecimal.valueOf(50000)),
OrderStatus.PLACED
);
}
public static Order confirmedOrder() {
defaultExistingOrder();
order.confirm();
order;
}
Order {
defaultExistingOrder();
order.cancel();
order;
}
Order {
Order.forExisting(
OrderId.of(id),
(),
Money.of(amount),
status
);
}
}
핵심 규칙:
- 위치:
domain/src/testFixtures/java/com/ryuqq/fixture/domain/
final 클래스
private 생성자 (인스턴스 생성 방지)
static 메서드만 사용
- Domain 객체만 의존 (Application/Adapter 의존 금지)
3. Application Fixture
package com.ryuqq.fixture.application.command;
import com.ryuqq.application.order.dto.command.PlaceOrderCommand;
import java.math.BigDecimal;
public final class PlaceOrderCommandFixture {
private PlaceOrderCommandFixture() {
throw new AssertionError("Utility class - do not instantiate");
}
public static PlaceOrderCommand defaultCommand() {
return new PlaceOrderCommand(
1L,
BigDecimal.valueOf(50000)
);
}
public static PlaceOrderCommand customCommand(Long customerId, BigDecimal amount) {
return new PlaceOrderCommand(customerId, amount);
}
}
4. Request Fixture (REST API)
package com.ryuqq.fixture.adapter.rest;
import com.ryuqq.adapter.in.rest.order.dto.PlaceOrderRequest;
import java.math.BigDecimal;
import java.util.List;
public final class OrderRequestFixture {
private OrderRequestFixture() {
throw new AssertionError("Utility class - do not instantiate");
}
public static PlaceOrderRequest validCreateRequest() {
return new PlaceOrderRequest(
1L,
List.of(OrderItemRequestFixture.validItemRequest())
);
}
public static PlaceOrderRequest invalidRequest_EmptyItems() {
return new PlaceOrderRequest(1L, List.of());
}
public static PlaceOrderRequest invalidRequest_NullCustomerId() {
return new PlaceOrderRequest(
null,
List.of(OrderItemRequestFixture.validItemRequest())
);
}
}
5. @Sql 테스트 데이터 (DML만!)
DELETE FROM order_items;
DELETE FROM orders;
DELETE FROM customers;
INSERT INTO customers (customer_id, name, email)
OVERRIDING SYSTEM VALUE
VALUES (1, 'Alice', 'alice@example.com');
INSERT INTO customers (customer_id, name, email)
OVERRIDING SYSTEM VALUE
VALUES (2, 'Bob', 'bob@example.com');
INSERT INTO orders (order_id, customer_id, status, total_amount, order_date)
OVERRIDING SYSTEM VALUE
VALUES (100, 1, 'PENDING', 10000, '2024-01-01');
INSERT INTO orders (order_id, customer_id, status, total_amount, order_date)
OVERRIDING SYSTEM VALUE
VALUES (101, 1, 'CONFIRMED', 20000, '2024-01-02');
INSERT INTO order_items (order_item_id, order_id, product_id, quantity, price)
OVERRIDING SYSTEM VALUE
VALUES (1000, 100, 200, 2, 5000);
핵심 규칙:
- DDL 금지:
CREATE TABLE, ALTER TABLE 절대 금지 (Flyway 책임)
- DML만:
INSERT, UPDATE, DELETE만 허용
- 클린업 먼저: DELETE → INSERT 순서
- FK 순서: 부모 먼저, 자식 나중
6. Gradle build.gradle 설정
// domain/build.gradle
plugins {
id 'java-library'
id 'java-test-fixtures' // ⭐ testFixtures 활성화
}
dependencies {
// Test
testImplementation libs.junit.jupiter
testImplementation libs.archunit.junit5
// Test Fixtures - Domain은 순수 Java만
// NO external dependencies
}
// application/build.gradle
plugins {
id 'java-library'
id 'java-test-fixtures'
}
dependencies {
api project(':domain')
// Test
testImplementation libs.spring.boot.starter.test
testImplementation testFixtures(project(':domain')) // ⭐ Domain Fixture
// Test Fixtures
testFixturesApi project(':domain')
testFixturesApi testFixtures(project(':domain'))
}
// bootstrap/build.gradle (Integration Test)
dependencies {
testImplementation libs.spring.boot.starter.test
testImplementation libs.testcontainers.mysql
testImplementation libs.testcontainers.junit
// Fixtures
testImplementation testFixtures(project(':domain'))
testImplementation testFixtures(project(':application'))
testImplementation testFixtures(project(':adapter-in:rest-api'))
}
테스트 실행 흐름
1. TestContainers 시작
└─ Docker → MySQL 컨테이너 시작
2. Spring Boot 시작 (@SpringBootTest)
└─ 전체 Bean 로딩
3. Flyway 자동 실행 (spring.flyway.enabled=true)
└─ V1 → V2 → V3 ... (테이블 생성)
4. @Sql 실행 (각 테스트 메서드 전)
└─ INSERT 테스트 데이터
5. 테스트 메서드 실행
└─ TestRestTemplate → Controller → UseCase → Repository → DB
6. @Transactional 롤백
└─ @Sql 데이터 자동 삭제
7. 다음 테스트 메서드 (4~6 반복)
8. TestContainers 종료
└─ MySQL 컨테이너 삭제
Fixture 의존성 규칙
허용되는 의존성 (✅)
domain testFixtures
↓ 의존
domain (Production)
application testFixtures
↓ 의존 ↓ 의존
application domain testFixtures
adapter-in testFixtures
↓ 의존 ↓ 의존
adapter-in application testFixtures
금지된 의존성 (❌)
domain testFixtures → application testFixtures ❌ 역방향
application testFixtures → adapter-* testFixtures ❌ 역방향
adapter-in testFixtures → adapter-out testFixtures ❌ 교차 금지
의존성 매트릭스
| From ↓ / To → | domain Fixture | application Fixture | adapter Fixture |
|---|
| domain tests | ✅ | ❌ | ❌ |
| application tests | ✅ | ✅ | ❌ |
| adapter-in tests | ✅ | ✅ | ✅ (in만) |
| adapter-out tests | ✅ | ❌ | ✅ (out만) |
Zero-Tolerance 규칙
✅ MANDATORY (필수)
| 규칙 | 설명 |
|---|
@SpringBootTest(RANDOM_PORT) | 전체 컨텍스트 + 실제 HTTP 서버 |
| TestRestTemplate | 실제 HTTP 요청/응답 검증 |
@Transactional | 테스트 격리, 자동 롤백 |
@Sql DML만 | INSERT, UPDATE, DELETE만 |
@ActiveProfiles("test") | 테스트 전용 설정 |
@Testcontainers | 실제 DB (MySQL) |
| Fixture: final 클래스 | 상속 금지 |
| Fixture: private 생성자 | 인스턴스 생성 금지 |
| Fixture: static 메서드 | 유틸리티 패턴 |
*Fixture 접미사 | 네이밍 규칙 |
❌ PROHIBITED (금지)
| 항목 | 이유 |
|---|
| MockMvc | 가짜 HTTP, TestRestTemplate 사용 |
@Sql에 DDL | CREATE TABLE 금지, Flyway 책임 |
@WebMvcTest | 통합 테스트는 @SpringBootTest |
@MockBean 남발 | 실제 Bean 사용 (통합 테스트) |
| EntityManager.persist() | @Sql 사용, SQL 파일로 관리 |
@DirtiesContext | 느림, @Transactional 롤백 사용 |
| 역방향 Fixture 의존 | domain → application 금지 |
| H2 사용 | MySQL (TestContainers) 사용 |
패키지 구조
project/
├── domain/
│ ├── src/main/java/
│ ├── src/test/java/
│ └── src/testFixtures/java/
│ └── com/ryuqq/fixture/domain/
│ ├── OrderFixture.java
│ ├── ProductFixture.java
│ └── CustomerFixture.java
│
├── application/
│ ├── src/main/java/
│ ├── src/test/java/
│ └── src/testFixtures/java/
│ └── com/ryuqq/fixture/application/
│ ├── command/
│ │ └── PlaceOrderCommandFixture.java
│ └── response/
│ └── OrderResponseFixture.java
│
├── adapter-in/rest-api/
│ ├── src/main/java/
│ ├── src/test/java/
│ └── src/testFixtures/java/
│ └── com/ryuqq/fixture/adapter/rest/
│ └── OrderRequestFixture.java
│
├── adapter-out/persistence-mysql/
│ ├── src/main/java/
│ ├── src/test/java/
│ └── src/testFixtures/java/
│ └── com/ryuqq/fixture/adapter/persistence/
│ └── OrderEntityFixture.java
│
└── bootstrap/
└── src/test/
├── java/
│ └── com/ryuqq/bootstrap/
│ └── OrderIntegrationTest.java
└── resources/sql/
└── orders-test-data.sql
체크리스트 (Output Checklist)
Integration Test
Domain Fixture
Application Fixture
@Sql 파일
Gradle 설정
ArchUnit 테스트 체크리스트
Fixture 의존성 규칙 (9개)
Fixture 네이밍 컨벤션
| 패턴 | 용도 | 예시 |
|---|
default*() | 기본 객체 | defaultOrder(), defaultNewOrder() |
*WithStatus() | 특정 상태 | orderWithPendingStatus() |
custom*() | 커스텀 빌더 | customOrder(Long id, ...) |
invalid*() | 유효하지 않은 객체 | invalidOrder() (예외 테스트) |
valid*Request() | 유효한 요청 | validCreateRequest() |
invalid*Request_*() | 무효 요청 (사유) | invalidRequest_EmptyItems() |
참조 문서
- Integration Test Guide:
docs/coding_convention/05-testing/integration-testing/01_integration-testing-overview.md
- Test Fixtures Guide:
docs/coding_convention/05-testing/test-fixtures/01_test-fixtures-guide.md
- Test Fixtures ArchUnit:
docs/coding_convention/05-testing/test-fixtures/02_test-fixtures-archunit.md
- Flyway Guide:
docs/coding_convention/04-persistence-layer/mysql/config/flyway-guide.md