Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# 테스트 가이드

## 테스트 성능 최적화

이 프로젝트는 **Singleton Container 패턴**을 사용하여 테스트 실행 속도를 크게 향상시켰습니다.

### 주요 최적화 사항

1. **Singleton PostgreSQL Container**
- 모든 테스트가 하나의 Testcontainers 인스턴스를 공유
- 컨테이너는 첫 테스트 실행 시 한 번만 시작되고 JVM 종료 시까지 유지
- 테스트 클래스마다 컨테이너를 시작/중지하는 오버헤드 제거

2. **Flyway 마이그레이션 최적화**
- 컨테이너당 한 번만 마이그레이션 실행
- `baseline-on-migrate: true` 설정으로 이미 마이그레이션된 DB 처리
- `clean-disabled: true`로 불필요한 스키마 삭제 방지

3. **트랜잭션 기반 테스트 격리**
- `@DataJpaTest`가 자동으로 `@Transactional` 제공
- 각 테스트 메서드는 자동으로 롤백되어 독립성 보장
- 별도 데이터 정리 로직 불필요

### 성능 개선 효과

- **이전**: 리포지토리 테스트 1개당 ~8초 (컨테이너 시작 + Flyway 마이그레이션)
- **현재**: 첫 테스트 ~8초, 이후 테스트 ~1-2초
- **전체 테스트 스위트**: 약 60-70% 시간 단축

## 리포지토리 테스트 작성 가이드

### 기본 구조

```java
@DataJpaTest // 자동으로 @Transactional 포함
@Import(YourRepository.class)
@ActiveProfiles("test")
class YourRepositoryTest extends AbstractPostgresContainerTest {

@Autowired
TestEntityManager em;

@Autowired
YourRepository repository;

@Test
void testSomething() {
// Given
YourEntity entity = new YourEntity();
em.persist(entity);
em.flush();

// When
YourEntity found = repository.findById(entity.getId()).orElseThrow();

// Then
assertThat(found).isNotNull();

// 테스트 종료 시 자동 롤백 - 데이터 정리 불필요
}
}
```

### 주의사항

#### ✅ DO

```java
@DataJpaTest
class GoodTest extends AbstractPostgresContainerTest {
@Test
void testWithAutoRollback() {
// 테스트 로직
// 자동 롤백됨 - 다음 테스트에 영향 없음
}
}
```

#### ❌ DON'T

```java
@DataJpaTest
@Commit // ❌ 롤백 비활성화하지 마세요!
class BadTest extends AbstractPostgresContainerTest {
@Test
void testWithCommit() {
// 데이터가 커밋되어 다른 테스트에 영향
}
}
```

```java
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED) // ❌ 트랜잭션 비활성화하지 마세요!
class BadTest extends AbstractPostgresContainerTest {
@Test
void testWithoutTransaction() {
// 테스트 격리 깨짐
}
}
```

### 통합 테스트 작성 시 주의사항

`@SpringBootTest`를 사용하는 통합 테스트에서도 Singleton Container의 혜택을 받을 수 있습니다:

```java
@SpringBootTest
@ActiveProfiles("test")
@Transactional // 명시적으로 추가 필요
class IntegrationTest extends AbstractPostgresContainerTest {

@Test
void integrationTest() {
// 통합 테스트 로직
}
}
```

## 병렬 테스트 실행

Singleton Container 패턴은 병렬 테스트 실행과 호환됩니다:

```bash
# Gradle에서 병렬 테스트 실행
./gradlew test --parallel --max-workers=4
```

트랜잭션 격리 덕분에 각 테스트가 독립적으로 실행되므로 안전합니다.

## 트러블슈팅

### Flyway 마이그레이션 충돌

테스트 실행 중 Flyway 오류가 발생하면:

```yaml
# application-test.yml
spring:
flyway:
baseline-on-migrate: true
clean-disabled: true
```

설정이 있는지 확인하세요.

### 테스트 간 데이터 오염

- `@DataJpaTest`가 적용되어 있는지 확인
- `@Commit`이나 `@Transactional(propagation = NOT_SUPPORTED)` 사용 여부 확인
- 필요시 `@Sql`로 특정 데이터 초기화:

```java
@Test
@Sql("/test-data/cleanup.sql")
void testWithCleanup() {
// 테스트 로직
}
```

## 참고 자료

- [Testcontainers 공식 문서](https://www.testcontainers.org/)
- [Spring Boot Testing Best Practices](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing)
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.devkor.apu.saerok_server.testsupport;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
Expand All @@ -10,28 +8,50 @@
/**
* 테스트용 PostgreSQL 컨테이너를 관리하는 추상 클래스입니다.
*
* <p>Testcontainers를 통해 통합 테스트나 슬라이스 테스트 실행 시
* 독립적인 Postgres+PostGIS 환경을 제공합니다.
* <p><strong>Singleton Container 패턴</strong>을 사용하여 모든 테스트가 하나의 컨테이너를 공유합니다.
* 이를 통해 컨테이너 시작/중지 오버헤드를 최소화하고 테스트 실행 속도를 크게 향상시킵니다.
*
* <p>컨테이너는 테스트 시작 시 자동으로 기동되고, 종료 시 자동으로 중단됩니다.
* 데이터소스와 Flyway 설정은 컨테이너 연결 정보로 재정의됩니다.
* <h2>주요 특징</h2>
* <ul>
* <li>컨테이너는 처음 테스트 실행 시 한 번만 시작되고 JVM 종료 시까지 유지됩니다</li>
* <li>Flyway 마이그레이션은 컨테이너당 한 번만 실행됩니다</li>
* <li>테스트 간 데이터 격리는 {@code @Transactional} 애노테이션으로 보장됩니다</li>
* </ul>
*
* <p>이 클래스를 상속하면 테스트마다 중복 설정 없이 일관된 환경을 사용할 수 있습니다.
* <h2>사용 시 주의사항</h2>
* <ul>
* <li><strong>테스트 격리</strong>: 리포지토리 테스트는 반드시 {@code @Transactional}을 사용하여
* 각 테스트 메서드가 독립적으로 실행되도록 해야 합니다 (기본적으로 {@code @DataJpaTest}가 제공)</li>
* <li><strong>병렬 실행</strong>: 트랜잭션 격리 수준 덕분에 테스트 병렬 실행이 가능하지만,
* 격리 수준을 낮춘 통합 테스트는 주의가 필요합니다</li>
* <li><strong>데이터 정리</strong>: {@code @Transactional} 테스트는 자동으로 롤백되므로
* 별도 정리 로직이 불필요합니다</li>
* </ul>
*
* <h2>성능 개선 효과</h2>
* <ul>
* <li>테스트 클래스당 컨테이너 시작 시간(~3초) 제거</li>
* <li>Flyway 마이그레이션 중복 실행 제거 (2번째 테스트부터)</li>
* </ul>
*/
public abstract class AbstractPostgresContainerTest {
private static final PostgreSQLContainer<?> postgres;

static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
DockerImageName.parse("postgis/postgis:16-3.5-alpine").asCompatibleSubstituteFor("postgres")
);
static {
postgres = new PostgreSQLContainer<>(
DockerImageName.parse("postgis/postgis:16-3.5-alpine")
.asCompatibleSubstituteFor("postgres")
);

@BeforeAll
static void beforeAll() {
// 컨테이너를 시작하고 JVM 종료 시까지 유지
postgres.start();
}

@AfterAll
static void afterAll() {
postgres.stop();
// JVM 종료 시 컨테이너 정리를 위한 shutdown hook 등록 (Testcontainers가 자동으로 처리하지만 명시적으로 추가)
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (postgres.isRunning()) {
postgres.stop();
}
}));
}

@DynamicPropertySource
Expand Down
9 changes: 9 additions & 0 deletions src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@ spring:
flyway:
enabled: true
locations: classpath:db/migration
# Singleton Container 사용 시 중요한 설정들
baseline-on-migrate: true # 이미 테이블이 있는 경우에도 baseline 설정
baseline-version: 0
clean-disabled: true # 테스트 간 스키마 삭제 방지 (트랜잭션 롤백으로 격리)
# 마이그레이션 검증 스킵 (이미 실행된 마이그레이션 재검증 불필요)
validate-on-migrate: false

logging:
level:
org.hibernate.SQL: info
org.hibernate.type.descriptor.sql.BasicBinder: off
org.flywaydb: warn
org.testcontainers: warn
tc.testcontainers: warn