init: 클린 기반 auth 서버 설계
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
# Fixture / Factory 기준
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 테스트 코드에서 사용하는 fixture와 factory를 어떤 기준으로 만들고 사용할지 정의한다.
|
||||
|
||||
이 문서의 목표는 다음과 같다.
|
||||
|
||||
- 테스트 데이터 준비를 중복 없이 재사용 가능하게 만든다
|
||||
- fixture와 factory의 역할을 분리해 테스트 가독성을 높인다
|
||||
- mutable shared state로 인한 테스트 간 간섭을 줄인다
|
||||
- repository test, service test, `@SpringBootTest` 통합 테스트에서 테스트 데이터 준비 방식을 일관되게 만든다
|
||||
|
||||
JUnit Jupiter는 기본적으로 테스트 메서드마다 새로운 테스트 클래스 인스턴스를 만들어 테스트 간 상태 간섭을 줄이도록 설계되어 있고, Spring TestContext Framework는 테스트 인스턴스에 의존성을 주입해 fixture를 구성할 수 있게 한다. Spring Boot는 JPA 테스트에서 `TestEntityManager`를 보조 도구로 제공한다. 이 문서는 그런 공식 기능 위에, 프로젝트 차원의 fixture/factory 설계 규칙을 얹는 문서다.
|
||||
|
||||
## 2. 근거 수준
|
||||
|
||||
- Official: JUnit / Spring Framework / Spring Boot 공식 문서에서 직접 확인되는 내용
|
||||
- Official + Practice: 공식 테스트 lifecycle, test fixture DI, JPA test helper 위에 일반적인 실무 best practice를 결합한 내용
|
||||
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
|
||||
|
||||
중요하게, fixture / factory라는 용어 자체를 JUnit이나 Spring이 프로젝트 표준으로 정의해 주지는 않는다. 공식 문서는 테스트 인스턴스 lifecycle, 테스트 fixture에 대한 DI, JPA test helper를 제공할 뿐이고, 이 문서의 fixture/factory 구분은 그 위에 얹는 실무적 설계 규칙이다. 또한 이 문서에서 말하는 factory는 JUnit의 동적 테스트용 `@TestFactory`가 아니라, 테스트 데이터 생성 helper/factory를 뜻한다. JUnit의 `@TestFactory`는 런타임에 동적 테스트를 생성하는 별도의 기능이다.
|
||||
|
||||
## 3. 용어 정의
|
||||
|
||||
### 3.1 fixture
|
||||
|
||||
이 문서에서 fixture는 테스트가 읽기 쉬운 형태로 준비된 데이터/상태를 뜻한다. 예를 들어 “활성 사용자”, “삭제된 사용자”, “주문이 2개 있는 고객”처럼 시나리오 의미가 드러나는 준비물이 fixture다. 이 개념은 공식 프레임워크 용어라기보다 테스트 설계 용어이지만, JUnit의 per-method lifecycle과 Spring의 test fixture DI는 이런 준비물을 테스트마다 독립적으로 구성하는 데 맞춰져 있다.
|
||||
|
||||
### 3.2 factory
|
||||
|
||||
이 문서에서 factory는 새 테스트 데이터를 생성하는 helper를 뜻한다. fixture가 “이 테스트에서 필요한 상태 이름”에 가깝다면, factory는 “그 상태를 만들 수 있는 생성 도구”에 가깝다. 따라서 factory는 보통 매 호출마다 새 객체를 반환하고, fixture는 그 factory를 사용해 특정 시나리오를 설명하는 더 얇은 레이어가 된다. 이 구분은 공식 문서의 직접 규정이 아니라, JUnit의 테스트 격리 모델과 Spring 테스트 지원 위에 얹는 프로젝트 권장안이다.
|
||||
|
||||
## 4. 기본 원칙
|
||||
|
||||
### 4.1 fixture와 factory는 테스트 지원 코드이지, 테스트 대상이 아니다
|
||||
|
||||
fixture와 factory의 목적은 테스트를 짧게 만드는 것이 아니라 의도를 더 잘 드러내게 하는 것이다. Spring 테스트 문서는 DI가 테스트와 통합 테스트를 더 쉽게 만든다고 설명하지만, 그 목적은 wiring 편의이지 테스트 관심사를 숨기는 것이 아니다. 프로젝트에서는 fixture/factory가 테스트의 핵심 조건과 기대를 감추지 않아야 한다.
|
||||
|
||||
### 4.2 테스트에서 중요한 값은 숨기지 않고 드러낸다
|
||||
|
||||
fixture/factory는 반복되는 노이즈를 줄이기 위해 존재하지만, 테스트 결과를 바꾸는 핵심 입력까지 숨기면 테스트가 읽기 어려워진다. 따라서 프로젝트에서는 “이 테스트가 왜 통과/실패해야 하는가”를 결정하는 값은 테스트 본문에 남기고, 나머지 반복 필드만 fixture/factory가 채우게 한다. 이 규칙은 JUnit이 기본적으로 테스트 메서드 격리와 명확한 lifecycle을 제공한다는 점 위에 얹는 실무 best practice다.
|
||||
|
||||
### 4.3 기본값은 유효한 객체여야 한다
|
||||
|
||||
factory가 만드는 기본 객체는 특별한 목적이 없는 한 도메인상 유효한 상태여야 한다. invalid 상태가 필요하면 `invalidEmailUser()`, `deletedUser()`처럼 의도가 드러나는 별도 fixture/factory entrypoint로 표현한다. 이렇게 해야 테스트가 “무엇을 깨려는지”를 코드만 봐도 이해할 수 있다. 이 규칙은 공식 API가 직접 강제하는 것은 아니지만, Spring이 단위 테스트와 통합 테스트에서 DI를 통해 테스트 준비를 쉽게 하라고 설명하는 취지와 맞는 프로젝트 권장안이다.
|
||||
|
||||
### 4.4 mutable shared fixture는 기본 금지다
|
||||
|
||||
JUnit Jupiter의 기본 lifecycle은 `PER_METHOD`이며, 각 테스트 메서드 전에 새로운 테스트 인스턴스를 만든다. JUnit은 이것이 mutable test instance state로 인한 예기치 않은 부작용을 줄이기 위한 기본 동작이라고 설명한다. 프로젝트에서도 이 철학을 따라, mutable 엔티티나 변경 가능한 컬렉션을 static/shared fixture로 재사용하는 것을 기본 금지한다. 매 호출마다 새로운 객체를 만들어야 한다.
|
||||
|
||||
### 4.5 PER_CLASS lifecycle은 fixture 최적화 수단이 아니라 예외적 선택지다
|
||||
|
||||
JUnit은 `@TestInstance(PER_CLASS)`를 쓰면 같은 테스트 인스턴스를 재사용하게 되고, 이 경우 instance field 상태를 `@BeforeEach`나 `@AfterEach`에서 직접 정리해야 할 수 있다고 설명한다. 또한 기본 lifecycle을 바꾸는 것은 일관되지 않게 적용되면 fragile build를 만들 수 있다고 경고한다. 프로젝트에서는 fixture/factory 편의 때문에 `PER_CLASS`를 기본값으로 바꾸지 않는다.
|
||||
|
||||
## 5. fixture 기준
|
||||
|
||||
### 5.1 fixture 이름은 시나리오를 설명해야 한다
|
||||
|
||||
fixture 이름은 `user1`, `sampleOrder`, `data()`처럼 모호하면 안 되고, `activeUser()`, `deletedUser()`, `orderPendingPayment()`처럼 현재 테스트가 필요로 하는 상태를 설명해야 한다. fixture는 테스트 서사의 일부이므로, 생성 방식보다 상태 의미가 이름에 드러나야 한다. 이 규칙은 프로젝트 best practice다.
|
||||
|
||||
### 5.2 fixture는 최소한의 차이만 담아야 한다
|
||||
|
||||
fixture가 너무 많은 상태를 한 번에 끌고 오면 테스트마다 어떤 값이 핵심인지 구분하기 어려워진다. 따라서 `activeAdminUserWithExpiredPasswordAndTwoOrders()` 같은 거대한 fixture보다, 기본 유효 factory + 필요한 시나리오 fixture를 조합하는 방식을 선호한다. 이 규칙은 JUnit의 테스트 격리 모델과 Spring 테스트의 fixture DI 취지를 코드 가독성 관점으로 확장한 프로젝트 권장안이다.
|
||||
|
||||
### 5.3 fixture는 공유 상태를 캐시하지 않는다
|
||||
|
||||
fixture helper 내부에서 static mutable 객체를 재사용하거나, 이전 테스트에서 만든 엔티티를 보관했다가 다시 넘기지 않는다. JUnit 기본 lifecycle이 테스트 격리를 지향하는 이유와 어긋나기 때문이다. fixture는 매번 fresh object를 생성하거나, 적어도 immutable 값만 공유해야 한다.
|
||||
|
||||
### 5.4 persisted fixture와 transient fixture를 구분한다
|
||||
|
||||
JPA 테스트에서는 아직 저장되지 않은 객체와, DB에 persist/flush까지 된 객체의 의미가 다르다. Spring Boot의 `TestEntityManager`도 `persist`, `persistAndFlush`, `persistFlushFind` 같은 helper를 제공한다. 프로젝트에서는 `user()` 같은 transient fixture와 `persistedUser()` 같은 persisted fixture를 이름부터 구분하는 것을 권장한다.
|
||||
|
||||
## 6. factory 기준
|
||||
|
||||
### 6.1 factory는 호출할 때마다 새 객체를 반환한다
|
||||
|
||||
factory는 기본적으로 새 인스턴스 생성기여야 한다. JUnit의 per-method isolation 철학상, 테스트가 서로 영향을 주지 않으려면 각 테스트가 독립적인 객체를 받아야 한다. 따라서 factory는 static mutable singleton을 반환하거나 같은 엔티티 인스턴스를 재사용하지 않는다.
|
||||
|
||||
### 6.2 factory는 기본값을 채우고, 테스트는 중요한 차이만 override한다
|
||||
|
||||
좋은 factory는 도메인상 유효한 기본값을 제공하고, 테스트는 필요한 필드만 덮어쓴다. 이렇게 해야 테스트 본문이 짧아지고, 동시에 핵심 입력은 눈에 남는다. 프로젝트에서는 factory가 모든 필드를 강제로 매개변수로 받는 형태보다, sane defaults + override 조합을 선호한다. 이 규칙은 공식 문서의 직접 규정은 아니지만, Spring이 테스트에서 DI로 준비 부담을 줄이게 해 주는 방향과 맞는 실무 기준이다.
|
||||
|
||||
### 6.3 boolean flag 나열형 factory는 기본 금지다
|
||||
|
||||
`user(true, false, true, false)`처럼 boolean 파라미터가 늘어나는 factory는 테스트 의도를 숨긴다. 프로젝트에서는 의도가 다른 상태라면 별도 fixture entrypoint나 명시적 builder step으로 나누는 것을 기본값으로 둔다. 이 규칙은 테스트 가독성과 시나리오 설명력을 높이기 위한 프로젝트 권장안이다.
|
||||
|
||||
### 6.4 invalid factory는 명시적으로 이름 붙인다
|
||||
|
||||
유효한 기본 factory와 달리, 제약 위반이나 validation failure를 검증하는 invalid 객체는 `userWithInvalidEmail()`, `orderWithoutCustomer()`처럼 왜 invalid인지 이름에 드러나야 한다. repository test에서는 이런 invalid fixture를 `flush()`까지 가서 검증하는 경우가 많기 때문에, 이름이 더 중요하다. `TestEntityManager`가 persist/flush/find helper를 제공하는 점도 이런 테스트를 명시적으로 작성하도록 돕는다.
|
||||
|
||||
## 7. Spring 컨텍스트 테스트 기준
|
||||
|
||||
### 7.1 Spring test fixture DI는 허용되지만, fixture 자체를 bean으로 만들지는 않는다
|
||||
|
||||
Spring Framework는 테스트 인스턴스에 field, setter, constructor injection을 사용할 수 있다고 설명하고, test code에서는 field injection도 자연스러울 수 있다고 설명한다. 하지만 프로젝트에서는 fixture/factory 자체를 애플리케이션 빈으로 등록하는 것을 기본값으로 두지 않는다. 대부분의 fixture/factory는 `src/test` 안의 일반 테스트 지원 코드로 충분하다. Spring bean으로 올리는 것은 repository, clock, encoder처럼 실제 인프라 의존이 있는 경우에만 제한적으로 허용한다.
|
||||
|
||||
### 7.2 Spring 컨텍스트를 쓰는 테스트에서도 fixture/factory는 테스트 소스에 둔다
|
||||
|
||||
test fixture DI는 테스트 클래스에 이미 만들어진 bean을 주입하는 수단이지, 테스트 데이터를 애플리케이션 production bean처럼 관리하라는 뜻은 아니다. 프로젝트에서는 fixture/factory를 production source set에 두지 않고, 기본적으로 `src/test/java` 또는 테스트 전용 support 패키지에 둔다. 이는 Spring test fixture DI 공식 기능 위에 얹는 프로젝트 경계 규칙이다.
|
||||
|
||||
## 8. JPA / repository test 기준
|
||||
|
||||
### 8.1 JPA fixture는 영속성 상태를 의식해야 한다
|
||||
|
||||
JPA 테스트에서 객체가 새 객체인지, managed 상태인지, DB에 flush되었는지에 따라 의미가 달라진다. `TestEntityManager`는 바로 이런 테스트를 위해 `persist`, `flush`, `find`, `persistFlushFind` 같은 helper를 제공한다. 프로젝트에서는 repository test용 fixture/factory가 이 차이를 무시하지 않도록, `newUser()`, `persistedUser()`, `persistedUserAndClear()` 같은 식으로 상태를 구분해 제공하는 것을 권장한다.
|
||||
|
||||
### 8.2 DB round-trip 의미가 중요한 테스트에서는 flush/clear를 factory가 완전히 숨기지 않는다
|
||||
|
||||
fixture/factory가 너무 많은 것을 숨기면 repository test에서 중요한 `flush()`/`clear()` 타이밍이 보이지 않게 된다. JPA 테스트의 핵심은 종종 “실제 DB와 동기화한 뒤 다시 읽었을 때 무엇이 보이는가”이므로, persisted helper를 제공하더라도 테스트 본문에서 flush/clear가 왜 필요한지 설명 가능해야 한다. `TestEntityManager`가 제공하는 helper는 보조 도구이지, 테스트 의미를 가리는 추상화가 되어서는 안 된다.
|
||||
|
||||
## 9. 프로젝트 권장안
|
||||
|
||||
### 9.1 기본 구조
|
||||
|
||||
프로젝트의 기본 권장 구조는 다음과 같다.
|
||||
|
||||
- fixture: 시나리오 이름이 드러나는 얇은 helper
|
||||
- factory: 기본 유효 객체를 생성하는 재사용 도구
|
||||
- persisted factory: repository / `TestEntityManager`를 써서 DB 상태까지 준비하는 helper
|
||||
- 테스트 본문: 핵심 override와 assertion을 직접 드러냄
|
||||
|
||||
이 구조는 JUnit의 per-method 격리, Spring test fixture DI, Spring Boot `TestEntityManager` 역할을 함께 고려한 프로젝트 표준이다.
|
||||
|
||||
### 9.2 파일/패키지 권장안
|
||||
|
||||
프로젝트에서는 fixture/factory를 기본적으로 테스트 소스에 두고, 필요하면 다음처럼 나눈다.
|
||||
|
||||
- `...testsupport.fixture`
|
||||
- `...testsupport.factory`
|
||||
- `...testsupport.builder`
|
||||
- `...testsupport.persisted`
|
||||
|
||||
이는 공식 프레임워크 규칙은 아니지만, 테스트 지원 코드를 production 코드와 분리하고 책임을 드러내기 위한 프로젝트 권장안이다.
|
||||
|
||||
## 10. 금지 규칙
|
||||
|
||||
다음은 기본 금지다.
|
||||
|
||||
- mutable entity를 static/shared fixture로 재사용하는 것
|
||||
- fixture/factory가 테스트의 핵심 입력을 숨기는 것
|
||||
- boolean 나열형 factory로 상태 의미를 감추는 것
|
||||
- invalid 상태를 모호한 이름의 기본 fixture로 섞어 두는 것
|
||||
- persisted/transient 상태를 이름 없이 섞는 것
|
||||
- repository test에서 flush/clear 의미를 factory가 완전히 감춰 버리는 것
|
||||
- fixture/factory를 production source set에 두는 것
|
||||
- fixture/factory를 애플리케이션 bean으로 무분별하게 등록하는 것
|
||||
- `PER_CLASS` lifecycle을 fixture 편의 때문에 기본값처럼 사용하는 것
|
||||
|
||||
이 금지 규칙은 JUnit의 test instance lifecycle, Spring의 test fixture DI, Spring Boot의 JPA test helper 역할을 실무 규칙으로 압축한 것이다.
|
||||
|
||||
## 11. 체크리스트
|
||||
|
||||
다음 질문에 “예”로 답할 수 있어야 한다.
|
||||
|
||||
- fixture 이름이 시나리오 의미를 설명하는가?
|
||||
- factory는 매번 새 객체를 반환하는가?
|
||||
- 기본 factory가 유효한 객체를 만드는가?
|
||||
- invalid 상태는 별도 이름으로 드러나는가?
|
||||
- persisted fixture와 transient fixture가 구분되는가?
|
||||
- repository test에서 DB round-trip 의미가 중요한 지점이 테스트에 드러나는가?
|
||||
- fixture/factory가 테스트 핵심 입력을 과하게 숨기지 않는가?
|
||||
- 테스트 지원 코드가 production source가 아니라 test source에 있는가?
|
||||
- `PER_CLASS`나 shared mutable state에 의존하지 않는가?
|
||||
@@ -0,0 +1,200 @@
|
||||
# Mock 사용 기준
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 테스트에서 mock, spy, Spring 컨텍스트 bean override mock을 언제 사용하고 언제 사용하지 말아야 하는지 정의한다.
|
||||
|
||||
이 문서의 목표는 다음과 같다.
|
||||
|
||||
- mock을 협력 객체 경계에만 사용하도록 제한한다
|
||||
- 단위 테스트용 Mockito mock과 Spring 컨텍스트용 bean override mock을 구분한다
|
||||
- spy, static mock, 과도한 interaction verification 같은 취약한 패턴을 줄인다
|
||||
- 현재 Spring 기준에 맞게 `@MockBean`/`@SpyBean` 대신 `@MockitoBean`/`@MockitoSpyBean` 사용 원칙을 정한다
|
||||
|
||||
Spring Boot는 현재 Spring Framework의 `@MockitoBean`과 `@MockitoSpyBean`을 Spring 테스트 컨텍스트 안의 bean override 수단으로 안내하고 있고, Spring Boot의 기존 `@MockBean` / `@SpyBean`은 3.4.0부터 deprecated 되었으며 4.0.0에서 제거되었다고 설명한다. 본 프로젝트는 Spring Boot 4.0.3을 사용하므로 해당 어노테이션은 더 이상 컴파일되지 않는다.
|
||||
|
||||
## 2. 근거 수준
|
||||
|
||||
- Official: Spring Framework / Spring Boot / Mockito 공식 문서에서 직접 확인되는 내용
|
||||
- Official + Practice: 공식 기능 위에 일반적인 실무 best practice를 결합한 내용
|
||||
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
|
||||
|
||||
이번 문서는 Spring Framework의 `@MockitoBean` / `@MockitoSpyBean`, Spring Boot의 테스트 문서와 deprecation API, Mockito의 `MockitoExtension`, strict stubbing, spy, `verifyNoMoreInteractions()` 관련 문서를 기준으로 작성한다. Mockito는 `MockitoExtension`이 mocks를 초기화하고 strict stubbings를 처리한다고 설명하고, `STRICT_STUBS`를 highly recommended라고 설명한다. 또한 spy는 carefully and occasionally 사용해야 한다고 경고하고, `verifyNoMoreInteractions()`를 모든 테스트마다 쓰는 것은 권장하지 않는다고 설명한다.
|
||||
|
||||
## 3. 기본 원칙
|
||||
|
||||
### 3.1 mock은 “테스트하고 싶은 대상”이 아니라 “대상이 의존하는 외부 협력자”에만 사용한다
|
||||
|
||||
mock은 테스트 대상 자체를 대신하는 도구가 아니라, 대상이 호출하는 다른 경계를 제어하기 위한 도구다. Mockito가 제공하는 mock/spy/verification 도구는 협력 객체를 대체하거나 관찰하는 데 목적이 있고, Spring의 `@MockitoBean`도 `ApplicationContext` 안의 bean을 override 하는 기능으로 설명된다. 프로젝트에서는 mock을 “내가 검증하려는 클래스”가 아니라 “내가 검증하려는 클래스가 호출하는 외부 의존성”에만 사용한다.
|
||||
|
||||
### 3.2 mock은 단순하게, 적게 사용한다
|
||||
|
||||
Mockito는 non-standard mock settings는 too often 쓰지 말라고 설명하고, 테스트가 너무 많은 mock에 의존하면 코드를 단순하게 리팩터링하는 편이 낫다고 시사한다. 프로젝트에서도 mock 개수가 많아질수록 테스트 대상이 너무 많은 책임을 가진 신호로 본다. 기본 원칙은 적은 수의 단순한 mock이다.
|
||||
|
||||
### 3.3 strict stubbing을 기본값으로 본다
|
||||
|
||||
Mockito는 strict stubbing이 cleaner tests, reduced duplication, improved debuggability를 주며 `STRICT_STUBS`를 highly recommended라고 설명한다. 또한 `MockitoExtension`은 mocks 초기화와 strict stubbings 처리를 담당한다고 설명한다. 프로젝트 기본값은 “쓰이지 않는 stub을 허용하는 느슨한 테스트”가 아니라 strict stubbing 기준의 테스트다.
|
||||
|
||||
## 4. 언제 mock을 사용하는가
|
||||
|
||||
### 4.1 순수 단위 테스트에서 외부 협력자를 대체할 때 사용한다
|
||||
|
||||
DB, 메시지 브로커, 외부 API client, 메일 발송기, 파일 저장기처럼 테스트 대상이 호출하는 외부 협력자를 실제로 띄우고 싶지 않을 때 mock이 적합하다. 이때는 Spring 컨텍스트 없이 `MockitoExtension`과 `@Mock`을 사용하는 순수 단위 테스트가 기본이다. Mockito는 `MockitoExtension`이 JUnit Jupiter용 확장이라고 설명한다.
|
||||
|
||||
### 4.2 interaction 자체가 의미인 경계에서 사용한다
|
||||
|
||||
어떤 메서드가 “무엇을 반환했는가”보다 “외부 협력자에게 어떤 호출을 했는가”가 의미인 경우가 있다. 예를 들어 이벤트 발행, 알림 전송, 외부 client 호출, retry 없이 1회만 위임해야 하는 adapter 경계가 그렇다. 이런 경우 mock verification이 적합하다. 다만 Mockito도 `verifyNoMoreInteractions()`를 모든 테스트마다 쓰는 것은 권장하지 않는다고 하므로, 프로젝트에서는 의미 있는 interaction만 검증한다.
|
||||
|
||||
### 4.3 Spring 컨텍스트 테스트에서는 bean override가 정말 필요할 때만 사용한다
|
||||
|
||||
`@MockitoBean`과 `@MockitoSpyBean`은 테스트의 `ApplicationContext` 안에서 bean을 mock/spy로 override하는 기능이다. 따라서 `@SpringBootTest`, slice test 같은 컨텍스트 테스트에서 특정 bean만 대체해야 할 때 적합하다. 하지만 이것은 “컨텍스트를 띄운 상태”를 전제로 하므로, 순수 단위 테스트에서 기본값이 되어서는 안 된다.
|
||||
|
||||
## 5. 언제 mock을 사용하지 않는가
|
||||
|
||||
### 5.1 엔티티, 값 객체, DTO에는 기본적으로 mock을 쓰지 않는다
|
||||
|
||||
엔티티, 값 객체, DTO는 테스트 대상 도메인 모델이므로 실제 객체를 만들어 쓰는 편이 더 자연스럽다. Mockito의 partial mock/spy 관련 문서도 partial mock이 대체로 설계 냄새라고 설명한다. 프로젝트에서는 도메인 모델을 mock으로 대체하지 않고, 실제 fixture/factory로 만든다.
|
||||
|
||||
### 5.2 repository test에서는 repository 자체를 mock하지 않는다
|
||||
|
||||
repository test의 목적은 DB 의미, 매핑, query, 제약을 검증하는 것이다. 이 문맥에서 repository를 mock으로 바꾸면 영속성 경계를 검증하지 못한다. 따라서 repository test는 `@DataJpaTest`와 실제 repository/DB를 사용하고, repository를 mock하는 것은 service 단위 테스트에서만 허용한다. 이 기준은 앞서 정한 repository test 문서와 일관된 프로젝트 규칙이다.
|
||||
|
||||
### 5.3 @SpringBootTest가 필요한 이유가 없는 테스트에서는 Spring bean mock을 쓰지 않는다
|
||||
|
||||
Spring Boot는 slice test와 full application context test를 구분해 제공한다. 따라서 Spring 컨텍스트가 굳이 필요 없는 테스트에서 `@MockitoBean`까지 사용하면 테스트가 과도하게 무거워진다. 프로젝트에서는 Spring bean mock보다 plain Mockito mock을 먼저 검토한다.
|
||||
|
||||
## 6. 단위 테스트에서의 기본 사용 기준
|
||||
|
||||
### 6.1 기본 조합은 MockitoExtension + @Mock
|
||||
|
||||
JUnit Jupiter 기반 Mockito 테스트의 기본 조합은 `@ExtendWith(MockitoExtension.class)`와 `@Mock`이다. Mockito는 `MockitoExtension`이 mocks를 초기화하고 strict stubbings를 처리한다고 설명한다. 프로젝트에서도 순수 단위 테스트의 기본 시작점은 이 조합이다.
|
||||
|
||||
### 6.2 @InjectMocks는 편의 수단으로만 사용한다
|
||||
|
||||
Mockito는 `@InjectMocks`가 constructor injection → setter injection → field injection 순서로 mock을 주입하려고 시도한다고 설명한다. 프로젝트에서는 `@InjectMocks`를 금지하지는 않지만, 테스트 대상 생성이 중요한 테스트에서는 명시적 생성자 호출을 더 선호한다. 그래야 의존성이 바뀌었을 때 테스트 코드에서 더 분명하게 드러난다. `@InjectMocks`는 보일러플레이트를 줄이는 편의 수단으로만 사용한다.
|
||||
|
||||
### 6.3 mock은 필요한 호출만 stub한다
|
||||
|
||||
Mockito는 strict stubbing이 cleaner tests를 만든다고 설명한다. 프로젝트에서는 미래를 대비한 과잉 stub, “혹시 몰라서 미리 깔아 두는 stub”, 실제로 사용되지 않는 stub을 금지한다. 테스트는 현재 시나리오에 필요한 stub만 가져야 한다.
|
||||
|
||||
## 7. verification 기준
|
||||
|
||||
### 7.1 state verification이 충분하면 interaction verification을 남발하지 않는다
|
||||
|
||||
mock verification은 유용하지만, 모든 테스트를 “호출 횟수 검사” 중심으로 만들 필요는 없다. Mockito도 `verifyNoMoreInteractions()`를 모든 테스트마다 쓰는 것은 권장하지 않는다고 설명한다. 프로젝트에서는 반환값/상태 변화로 충분한 테스트라면 그쪽을 우선하고, interaction 검증은 외부 경계 의미가 분명할 때만 사용한다.
|
||||
|
||||
### 7.2 verifyNoMoreInteractions()는 기본 금지다
|
||||
|
||||
Mockito는 `verifyNoMoreInteractions()`를 every test method에 쓰는 것을 권장하지 않는다고 분명히 말한다. 프로젝트에서는 이 메서드를 기본 assertion처럼 붙이지 않는다. 정말로 “추가 호출이 있으면 안 된다”가 비즈니스 의미인 경우에만 제한적으로 사용한다.
|
||||
|
||||
### 7.3 ArgumentCaptor는 verification을 완성하는 용도로만 사용한다
|
||||
|
||||
Mockito는 `ArgumentCaptor`를 verification과 함께 쓰는 것을 권장하고, stubbing에 쓰면 가독성과 defect localization이 나빠질 수 있다고 설명한다. 프로젝트에서도 `ArgumentCaptor`는 호출된 인자의 값을 마지막에 확인하는 용도로만 사용하고, stub 조건을 억지로 만드는 데는 기본적으로 사용하지 않는다.
|
||||
|
||||
## 8. spy 기준
|
||||
|
||||
### 8.1 spy는 기본 선택지가 아니다
|
||||
|
||||
Mockito는 real spy를 carefully and occasionally 사용하라고 설명하고, partial mock은 대체로 code smell이며 새롭고 잘 설계된 코드에는 권하지 않는다고 말한다. 프로젝트에서도 spy는 기본 선택지가 아니라 레거시 코드, 3rd-party 인터페이스, 점진적 리팩터링 같은 예외 상황에서만 사용한다.
|
||||
|
||||
### 8.2 spy stubbing에는 when(...)보다 doReturn(...) 계열을 우선한다
|
||||
|
||||
Mockito는 spy에서 `when(spy.method())`가 실제 메서드를 호출해 부작용을 일으킬 수 있으므로, `doReturn` / `doThrow` / `doNothing` 같은 계열을 고려하라고 설명한다. 프로젝트에서는 spy를 써야 한다면 stub 방식도 `doReturn(...).when(spy)...` 기본값으로 둔다.
|
||||
|
||||
### 8.3 @MockitoSpyBean은 더 신중히 쓴다
|
||||
|
||||
Spring의 `@MockitoSpyBean`은 기존 bean 인스턴스를 감싸는 방식이고, scoped proxy에는 사용할 수 없으며, non-singleton bean을 spy해도 singleton처럼 취급될 수 있다. 따라서 프로젝트에서는 `@MockitoSpyBean`을 넓은 컨텍스트 테스트에서 기본값으로 두지 않는다. 정말로 실제 bean 동작 일부만 감시해야 할 때만 제한적으로 사용한다.
|
||||
|
||||
## 9. Spring 컨텍스트에서의 mock 기준
|
||||
|
||||
### 9.1 새 기준은 @MockitoBean, @MockitoSpyBean
|
||||
|
||||
Spring Framework는 `@MockitoBean`과 `@MockitoSpyBean`을 테스트의 `ApplicationContext` bean override 용도로 제공한다. Spring Boot 문서도 이 어노테이션들을 안내하고 있다. 프로젝트에서는 Spring 컨텍스트 테스트에서 bean override가 필요하면 이 둘을 기본값으로 사용한다.
|
||||
|
||||
### 9.2 @MockBean, @SpyBean은 신규 코드 기본값으로 쓰지 않는다
|
||||
|
||||
Spring Boot API 문서는 `@MockBean`과 관련 Boot Mockito 테스트 지원이 3.4.0부터 deprecated 되었고, 4.0.0에서 제거되었으며 `MockitoBean`/`MockitoSpyBean`으로 대체하라고 설명한다. 본 프로젝트는 Spring Boot 4.0.3 기반이므로 `@MockBean`/`@SpyBean`은 더 이상 사용 가능하지 않다. 신규 테스트는 `@MockitoBean`/`@MockitoSpyBean`을 사용하고, 기존 테스트도 동일하게 이전한다.
|
||||
|
||||
### 9.3 @MockitoBean은 bean override가 필요한 테스트에만 사용한다
|
||||
|
||||
`@MockitoBean`은 bean을 `REPLACE_OR_CREATE` 전략으로 override하고, `enforceOverride = true`를 주면 반드시 기존 bean이 있어야만 교체하도록 바꿀 수 있다. 프로젝트에서는 bean이 없으면 새 mock을 조용히 만들어 버리는 기본 동작이 테스트 의도를 흐릴 수 있으므로, “반드시 기존 bean을 대체해야 한다”는 테스트에는 `enforceOverride = true`를 검토한다.
|
||||
|
||||
### 9.4 같은 bean을 mock하는 테스트는 필드 이름과 qualifier를 일관되게 유지한다
|
||||
|
||||
Spring Framework는 field 이름이나 qualifier가 컨텍스트 분리에 영향을 줄 수 있고, 같은 bean을 여러 테스트에서 mock/spy할 때 필드 이름을 일관되게 유지하면 불필요한 새로운 `ApplicationContext` 생성을 줄일 수 있다고 설명한다. 프로젝트에서는 컨텍스트 캐시를 깨지 않기 위해 같은 bean mock 필드 이름을 가능하면 통일한다.
|
||||
|
||||
### 9.5 non-singleton bean mock/spy는 기본 금지다
|
||||
|
||||
Spring Framework는 `@MockitoBean`으로 non-singleton bean을 mock하면 singleton mock으로 대체되고, `@MockitoSpyBean`으로 non-singleton bean을 spy해도 singleton처럼 취급된다고 설명한다. 프로젝트에서는 prototype/scoped bean override mock을 기본 금지하고, 정말 필요하면 테스트 구조를 다시 설계하는 쪽을 우선한다.
|
||||
|
||||
## 10. static mock 기준
|
||||
|
||||
### 10.1 static mock은 예외적이고 짧게 사용한다
|
||||
|
||||
Mockito는 `MockedStatic`이 활성화된 정적 mock을 나타내며, 그 mock이 생성된 thread에만 영향을 주고, 다른 thread와 동시에 쓰는 것은 안전하지 않다고 설명한다. 프로젝트에서는 static mocking을 레거시나 외부 라이브러리 래핑 같은 예외 상황에서만 허용하고, try-with-resources로 scope를 매우 짧게 제한한다.
|
||||
|
||||
### 10.2 새 코드 설계에서는 static mock 대신 의존성 분리를 우선한다
|
||||
|
||||
static mock은 가능하더라도 thread-scoped이고 테스트를 더 취약하게 만들 수 있다. 프로젝트에서는 새 코드에서 시간, UUID, 외부 유틸 호출 같은 요소를 static method로 직접 부르기보다 bean/port로 분리해서 plain mock으로 대체 가능하게 만드는 것을 기본값으로 둔다. 이 부분은 Mockito의 static mock 제약 위에 얹는 프로젝트 권장안이다.
|
||||
|
||||
## 11. 프로젝트 권장안
|
||||
|
||||
### 11.1 기본 선택 순서
|
||||
|
||||
프로젝트의 기본 선택 순서는 다음과 같다.
|
||||
|
||||
- mock 없이 실제 객체로 테스트 가능하면 그렇게 한다
|
||||
- 외부 협력자만 plain Mockito mock으로 대체한다
|
||||
- Spring 컨텍스트가 정말 필요하면 `@MockitoBean`을 사용한다
|
||||
- spy는 예외적으로만 사용한다
|
||||
- static mock은 마지막 수단으로만 사용한다
|
||||
|
||||
이 순서는 Mockito가 spy/partial mock을 신중히 쓰라고 경고하는 점과, Spring이 bean override mock을 별도 기능으로 제공하는 점을 함께 반영한 프로젝트 규칙이다.
|
||||
|
||||
### 11.2 신규 Spring 테스트는 @MockitoBean 기준으로 작성한다
|
||||
|
||||
Spring Boot 3.4+ 기준에서는 기존 Boot `@MockBean` 계열보다 Spring Framework `@MockitoBean` 계열이 현재 공식 방향이다. 프로젝트의 신규 컨텍스트 테스트는 이 기준을 따른다.
|
||||
|
||||
### 11.3 단위 테스트는 strict, 명시적, 짧게 유지한다
|
||||
|
||||
프로젝트 단위 테스트 기본값은 다음과 같다.
|
||||
|
||||
- `MockitoExtension`
|
||||
- strict stubbing
|
||||
- 최소 stub
|
||||
- 의미 있는 verification만 수행
|
||||
- 가능하면 명시적 생성자 주입
|
||||
- spy / static mock / deep stub 회피
|
||||
|
||||
Mockito는 strict stubbing을 강하게 권장하고, deep stubs와 partial mocks를 regular clean code에서는 드물게만 써야 한다고 설명한다.
|
||||
|
||||
## 12. 금지 규칙
|
||||
|
||||
다음은 기본 금지다.
|
||||
|
||||
- 테스트 대상 자체를 mock하는 것
|
||||
- 엔티티, 값 객체, DTO를 기본적으로 mock하는 것
|
||||
- repository test에서 repository를 mock하는 것
|
||||
- 모든 테스트에 `verifyNoMoreInteractions()`를 습관적으로 붙이는 것
|
||||
- spy를 기본 선택지처럼 사용하는 것
|
||||
- spy에서 `when(spy.method())`로 실제 메서드 부작용을 일으키는 것
|
||||
- Spring 컨텍스트 테스트 신규 코드에 `@MockBean` / `@SpyBean`을 기본값으로 사용하는 것
|
||||
- prototype/scoped bean을 무심코 `@MockitoBean` / `@MockitoSpyBean`으로 override하는 것
|
||||
- static mock을 긴 scope로 유지하거나 병렬 테스트에서 안전하다고 가정하는 것
|
||||
|
||||
이 금지 규칙은 Mockito의 spy/verification/static mock 주의사항과 Spring의 bean override 문서를 실무 규칙으로 압축한 것이다.
|
||||
|
||||
## 13. 체크리스트
|
||||
|
||||
다음 질문에 “예”로 답할 수 있어야 한다.
|
||||
|
||||
- 이 mock은 테스트 대상이 아니라 외부 협력자를 대체하고 있는가?
|
||||
- plain unit test라면 Spring 컨텍스트 없이 `MockitoExtension`으로 충분한가?
|
||||
- stub은 현재 시나리오에 필요한 것만 있는가?
|
||||
- state verification으로 충분한데 interaction verification을 남발하고 있지 않은가?
|
||||
- `verifyNoMoreInteractions()`가 정말 필요한 의미를 가지는가?
|
||||
- spy를 쓰는 이유가 레거시/부분 대체 같은 예외 상황으로 설명되는가?
|
||||
- Spring 컨텍스트 mock이라면 `@MockitoBean` / `@MockitoSpyBean`을 사용하고 있는가?
|
||||
- 같은 bean mock의 field name/qualifier를 테스트 간 일관되게 유지하고 있는가?
|
||||
- non-singleton bean override나 scoped proxy spy 같은 위험한 경우를 피했는가?
|
||||
- static mock이 정말 마지막 수단인가?
|
||||
@@ -0,0 +1,193 @@
|
||||
# Repository Test 기준
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 Spring Data JPA 기반 repository test를 어떤 범위까지 검증하고, 어떤 방식으로 실행할지 정의한다.
|
||||
|
||||
이 문서의 목표는 다음과 같다.
|
||||
|
||||
- repository test의 관심사를 영속성 경계로 한정한다
|
||||
- 기본 실행 방식으로 `@DataJpaTest`를 사용하고, `@SpringBootTest`와 역할을 분리한다
|
||||
- 영속성 컨텍스트 1차 캐시 때문에 가짜로 통과하는 테스트를 줄인다
|
||||
- PostgreSQL 특화 query나 제약 검증이 필요한 경우, 실제 DB 계열과 맞는 환경에서 검증하게 만든다
|
||||
|
||||
Spring Boot는 `@DataJpaTest`가 JPA components에만 초점을 맞춘 테스트이며, 기본적으로 `@Entity`와 Spring Data JPA repository를 스캔하고, 일반 `@Component` 빈은 로드하지 않는다고 설명한다. 또한 임베디드 DB가 classpath에 있으면 그것을 자동 구성하고, 기본적으로 transactional하게 실행된다고 설명한다.
|
||||
|
||||
## 2. 근거 수준
|
||||
|
||||
- Official: Spring Boot / Spring Data JPA / Spring Framework / Hibernate 공식 문서에서 직접 확인되는 내용
|
||||
- Official + Practice: 공식 기능 위에 일반적인 실무 best practice를 결합한 내용
|
||||
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
|
||||
|
||||
이번 문서는 `@DataJpaTest`, `TestEntityManager`, Spring 테스트 트랜잭션 rollback, Hibernate 1차 캐시/영속성 컨텍스트 문서를 기준으로 작성한다. Spring Data JPA는 repository query method의 transaction 설정 규칙도 별도로 설명하고 있다.
|
||||
|
||||
## 3. 기본 원칙
|
||||
|
||||
### 3.1 repository test는 repository 경계만 검증한다
|
||||
|
||||
repository test의 관심사는 서비스 유스케이스가 아니라 엔티티 매핑, repository method, JPQL/native query, flush 시점의 제약 위반, DB round-trip 이후의 조회 결과다. `@DataJpaTest`가 JPA components만 좁게 로드하도록 설계된 것도 이 경계를 전제로 한다. 프로젝트에서는 service orchestration, 외부 연동, 보안, MVC, application event 흐름은 repository test에 넣지 않는다.
|
||||
|
||||
### 3.2 repository test의 기본값은 @DataJpaTest
|
||||
|
||||
Spring Boot는 `@DataJpaTest`가 Data JPA 테스트에 필요한 auto-configuration만 켜고, 엔티티와 repository만 중심으로 로드한다고 설명한다. 따라서 repository test의 기본 어노테이션은 `@DataJpaTest`다. `@SpringBootTest`는 전체 애플리케이션 조립이 필요한 경우에만 예외적으로 사용하고, repository test의 기본값으로 두지 않는다.
|
||||
|
||||
### 3.3 repository test는 “메서드가 호출된다”가 아니라 “DB 의미가 맞다”를 검증해야 한다
|
||||
|
||||
JPA/Hibernate는 영속성 컨텍스트를 1차 캐시로 유지하며, Hibernate는 이를 generally “repeatable read” persistence context라고 설명한다. 따라서 같은 트랜잭션 안에서 entity를 다시 읽을 때, 실제 DB round-trip 없이 메모리 상태만 보게 될 수 있다. 프로젝트에서는 repository test가 정말 DB 의미를 검증해야 한다면, 필요 지점에서 `flush()`와 `clear()`를 사용해 영속성 컨텍스트 환상을 걷어낸 뒤 검증하는 것을 기본 원칙으로 둔다.
|
||||
|
||||
## 4. 언제 repository test를 작성하는가
|
||||
|
||||
### 4.1 다음은 repository test의 대표 대상이다
|
||||
|
||||
repository test의 대표 대상은 다음과 같다.
|
||||
|
||||
- 엔티티 매핑이 실제 스키마와 맞는지
|
||||
- derived query method가 기대한 조건으로 동작하는지
|
||||
- JPQL/native query가 기대한 결과를 반환하는지
|
||||
- unique/fk/check/not null 같은 DB 제약이 flush 시점에 올바르게 드러나는지
|
||||
- soft delete, partial index 전제 predicate, 정렬/페이지 query가 의도대로 동작하는지
|
||||
|
||||
이들은 모두 JPA repository/EntityManager 경계의 책임이며, `@DataJpaTest`가 좁게 검증하기에 적합한 주제다.
|
||||
|
||||
### 4.2 다음은 repository test의 기본 대상이 아니다
|
||||
|
||||
다음은 repository test의 기본 대상이 아니다.
|
||||
|
||||
- 서비스 유스케이스 전체 흐름
|
||||
- 여러 repository를 묶는 트랜잭션 정책
|
||||
- 보안 컨텍스트와 인증 인가
|
||||
- MVC 요청/응답 변환
|
||||
- 외부 API 연동과 메시징 흐름
|
||||
|
||||
이런 항목은 `@SpringBootTest`, slice test, 혹은 더 상위 통합 테스트의 관심사다. `@DataJpaTest`가 일반 `@Component`를 로드하지 않는다는 점도 이 구분과 맞는다.
|
||||
|
||||
## 5. 기본 실행 방식 기준
|
||||
|
||||
### 5.1 기본 어노테이션은 @DataJpaTest
|
||||
|
||||
Spring Boot는 `@DataJpaTest`가 JPA test에 초점을 맞추고, 엔티티와 repository를 스캔하며, 임베디드 DB가 있으면 그것을 자동 구성한다고 설명한다. 또한 기본적으로 transactional하게 실행되고, Spring 테스트 트랜잭션은 종료 시 기본 rollback된다. 프로젝트에서는 repository test 클래스의 기본 시작점을 `@DataJpaTest`로 둔다.
|
||||
|
||||
### 5.2 임베디드 DB 기본값을 무심코 신뢰하지 않는다
|
||||
|
||||
`@DataJpaTest`는 기본적으로 임베디드 DB를 구성할 수 있다. 하지만 repository가 PostgreSQL dialect, native query, JSONB, partial index, window function, locking clause, case sensitivity 차이 같은 DB 고유 동작에 의존한다면, 임베디드 DB만으로는 신뢰도가 부족할 수 있다. Spring Boot는 실제 DB를 선호하면 `@AutoConfigureTestDatabase`로 대체 전략을 제어할 수 있다고 설명한다. 프로젝트에서는 DB 특화 기능이 있는 repository test는 실제 운영 DB 계열로 검증하는 것을 기본 권장안으로 둔다.
|
||||
|
||||
### 5.3 @SpringBootTest는 repository test의 예외 경로다
|
||||
|
||||
repository 자체는 `@DataJpaTest`로 충분한 경우가 대부분이다. 다만 repository가 Boot auto-configuration, custom converter, listener, 여러 인프라 bean과 강하게 얽혀 있고 그 조합 자체를 검증해야 한다면 예외적으로 `@SpringBootTest`를 사용할 수 있다. 하지만 이 경우도 관심사는 여전히 repository 경계여야 하며, 단순히 편하다는 이유로 full context를 올리지는 않는다. 이 기준은 `@DataJpaTest`의 공식 역할 위에 얹는 프로젝트 best practice다.
|
||||
|
||||
## 6. 트랜잭션 기준
|
||||
|
||||
### 6.1 repository test는 기본적으로 rollback된다
|
||||
|
||||
Spring 테스트 문서는 transactional test가 기본적으로 종료 후 rollback된다고 설명한다. Spring Boot 문서도 `@DataJpaTest`가 기본적으로 transactional하게 동작한다고 설명한다. 따라서 repository test는 기본적으로 test isolation을 위해 rollback을 기대할 수 있다.
|
||||
|
||||
### 6.2 commit이 필요한 테스트만 예외적으로 @Commit 또는 @Rollback(false)를 사용한다
|
||||
|
||||
Spring은 `@Rollback(false)` 또는 `@Commit`으로 테스트 트랜잭션을 commit하도록 바꿀 수 있다고 설명한다. 프로젝트에서는 DB trigger, 외부 관측, 별도 세션에서만 보이는 결과, commit 이후 동작을 검증해야 할 때만 예외적으로 commit 테스트를 허용한다. 기본값은 rollback이다.
|
||||
|
||||
### 6.3 repository query method 자체의 transaction 설정을 혼동하지 않는다
|
||||
|
||||
Spring Data JPA는 declared query methods와 default methods에는 transaction configuration이 기본 적용되지 않으며, 필요하면 repository interface에 `@Transactional`을 명시해야 한다고 설명한다. 다만 `@DataJpaTest` 안에서는 테스트 메서드 자체가 트랜잭션 안에서 실행되므로, repository query method의 transaction 유무와 테스트 트랜잭션의 존재를 혼동하면 안 된다. 프로젝트에서는 repository method의 production transaction semantics를 검증하려는 테스트라면, 테스트 메서드 트랜잭션에 가려지지 않는지 먼저 확인한다.
|
||||
|
||||
## 7. 검증 방식 기준
|
||||
|
||||
### 7.1 저장 직후 검증이 아니라 flush() 이후 의미를 본다
|
||||
|
||||
영속성 컨텍스트 안에서 `save()`만 호출하고 바로 필드를 확인하면, 실제 SQL 실행이나 DB 제약 위반이 드러나지 않을 수 있다. Hibernate는 persistence context가 1차 캐시로 동작한다고 설명한다. 따라서 unique/fk/check/not null 위반, DB generated value, trigger, native query 결과를 보려면 `flush()`를 통해 DB와 동기화한 뒤 검증하는 것을 기본으로 한다.
|
||||
|
||||
### 7.2 조회 의미를 검증할 때는 필요하면 clear()까지 사용한다
|
||||
|
||||
같은 persistence context 안에서는 이미 읽은 엔티티가 다시 반환될 수 있다. 따라서 repository test가 정말 DB round-trip 이후의 조회 semantics를 보고 싶다면, `flush()` 후 `clear()`를 통해 1차 캐시를 비우고 다시 조회해야 한다. 프로젝트에서는 “쿼리가 실제로 원하는 row를 다시 읽어오는가”를 검증할 때 `clear()`를 적극적으로 사용한다.
|
||||
|
||||
### 7.3 예외 검증은 가능한 한 flush 시점까지 진행한다
|
||||
|
||||
제약 위반은 보통 DB에 SQL이 나가야 드러난다. 따라서 repository test에서 `assertThatThrownBy(() -> repository.save(entity))`처럼 save 호출만 감싸는 패턴은 충분하지 않을 수 있다. 프로젝트에서는 제약/매핑 오류 테스트를 `save` + `flush` 또는 `persistAndFlush` 수준까지 진행한 뒤 검증하는 것을 기본값으로 둔다. 이 규칙은 JPA flush semantics와 1차 캐시 특성을 근거로 한 best practice다.
|
||||
|
||||
## 8. TestEntityManager 기준
|
||||
|
||||
### 8.1 TestEntityManager는 repository test 보조 도구로 허용한다
|
||||
|
||||
Spring Boot는 `TestEntityManager`를 JPA 테스트용 대안 `EntityManager`로 제공하며, `persist`, `flush`, `find` 같은 testing helper를 제공한다고 설명한다. 프로젝트에서는 fixture seed, flush/clear, ID 확보, 영속성 컨텍스트 제어가 자주 필요할 때 `TestEntityManager` 사용을 허용한다.
|
||||
|
||||
### 8.2 다만 repository test의 중심은 여전히 repository여야 한다
|
||||
|
||||
`TestEntityManager`는 보조 도구이지 테스트 대상이 아니다. repository method를 검증하는 테스트가 `EntityManager` 호출로 가득 차면, 결국 repository를 우회한 테스트가 되기 쉽다. 프로젝트에서는 seed와 보조 검증 정도에만 쓰고, 핵심 assertion은 repository method 결과에 두는 것을 원칙으로 한다. 이 부분은 공식 API 역할 설명 위에 얹는 프로젝트 best practice다.
|
||||
|
||||
## 9. 데이터 준비 기준
|
||||
|
||||
### 9.1 repository test 데이터는 테스트 의도에 필요한 최소한만 준비한다
|
||||
|
||||
repository test는 SQL semantics를 검증하는 테스트이므로, 데이터가 많다고 좋은 것이 아니다. 정렬, 필터, unique, soft delete, join 조건을 드러내는 최소 사례 집합이 가장 좋다. 이 기준은 공식 문서가 직접 규정하는 항목은 아니지만, `@DataJpaTest`의 좁은 목적과 빠른 피드백에 맞는 실무 best practice다.
|
||||
|
||||
### 9.2 테스트마다 필요한 데이터는 독립적으로 준비한다
|
||||
|
||||
기본 rollback이 되더라도, 테스트끼리 순서 의존적인 데이터 준비를 하면 의도가 흐려진다. 프로젝트에서는 각 테스트가 자기 전제 데이터를 스스로 준비하게 하고, 외부 상태나 이전 테스트의 삽입 결과에 의존하지 않게 한다. 이는 Spring 테스트의 기본 rollback 모델과 맞는 프로젝트 규칙이다.
|
||||
|
||||
## 10. 무엇을 검증해야 하는가
|
||||
|
||||
### 10.1 repository test는 아래 항목을 우선 검증한다
|
||||
|
||||
프로젝트 기준으로 repository test가 특히 잘 검증해야 하는 것은 다음과 같다.
|
||||
|
||||
- 엔티티와 테이블/컬럼 매핑
|
||||
- 연관관계 매핑과 cascade로 인해 실제 SQL이 기대대로 나가는지
|
||||
- derived query method의 조건 해석
|
||||
- JPQL/native query의 결과 정확성
|
||||
- soft delete predicate, 정렬, pagination query
|
||||
- partial unique index, FK, check, not null 같은 DB 무결성 위반 드러남
|
||||
- flush 이후 다시 조회했을 때도 상태가 맞는지
|
||||
|
||||
이 항목들은 모두 영속성 경계의 책임이며, repository test에 가장 적합하다.
|
||||
|
||||
### 10.2 반대로 service policy는 repository test에서 검증하지 않는다
|
||||
|
||||
예외 변환, 유스케이스 조합, 외부 연동과 결합된 정책은 repository test에서 검증하지 않는다. 그런 항목은 상위 통합 테스트의 책임이다. repository test가 이 범위를 침범하면 `@DataJpaTest`의 좁은 장점이 사라진다.
|
||||
|
||||
## 11. 프로젝트 권장안
|
||||
|
||||
### 11.1 repository test의 기본 템플릿
|
||||
|
||||
프로젝트의 기본 repository test 템플릿은 다음과 같다.
|
||||
|
||||
- `@DataJpaTest`
|
||||
- 필요 시 실제 DB 계열 사용
|
||||
- fixture 준비
|
||||
- `repository.save(...)`
|
||||
- 필요 시 `flush()`, `clear()`
|
||||
- repository로 다시 조회
|
||||
- DB 의미 기준 assertion
|
||||
|
||||
이 흐름은 Spring Boot의 JPA slice test와 Hibernate 1차 캐시 특성을 함께 고려한 프로젝트 기본값이다.
|
||||
|
||||
### 11.2 PostgreSQL 의존 쿼리는 PostgreSQL로 검증한다
|
||||
|
||||
partial index 전제, JSONB, native query, locking clause, window function, case sensitivity, timestamp handling처럼 PostgreSQL 의미에 의존하는 repository test는 임베디드 대체 DB보다 실제 PostgreSQL 계열 환경에서 검증하는 것을 기본 권장안으로 둔다. Spring Boot가 실제 DB 사용을 위한 `@AutoConfigureTestDatabase` 제어를 제공하는 점과도 맞다.
|
||||
|
||||
## 12. 금지 규칙
|
||||
|
||||
다음은 기본 금지다.
|
||||
|
||||
- repository test 기본값으로 `@SpringBootTest` 사용
|
||||
- 순수 서비스 정책 테스트를 repository test에 넣는 것
|
||||
- `save()` 직후 영속성 컨텍스트 상태만 보고 DB 검증이 끝났다고 생각하는 것
|
||||
- 제약 위반 테스트를 flush 없이 작성하는 것
|
||||
- 1차 캐시 때문에 다시 읽은 엔티티를 실제 DB 조회 결과로 오해하는 것
|
||||
- PostgreSQL 특화 query를 임베디드 DB만으로 신뢰하는 것
|
||||
- soft delete, 정렬, pagination query를 최소 데이터셋 없이 대충 검증하는 것
|
||||
- bulk data seed나 복잡한 service 조립을 repository test에 끌어오는 것
|
||||
- repository test에서 repository를 거의 쓰지 않고 `EntityManager`만 사용하는 것
|
||||
|
||||
이 금지 규칙은 Spring Boot의 `@DataJpaTest`, Spring 테스트 rollback, Hibernate persistence context 의미를 실무 규칙으로 압축한 것이다.
|
||||
|
||||
## 13. 체크리스트
|
||||
|
||||
다음 질문에 “예”로 답할 수 있어야 한다.
|
||||
|
||||
- 이 테스트는 repository 경계만 검증하고 있는가?
|
||||
- 기본 어노테이션이 `@DataJpaTest`인가?
|
||||
- 실제 운영 DB 의미가 중요하면 테스트 DB도 그에 맞췄는가?
|
||||
- DB 제약/trigger/generated value를 검증할 때 `flush()`를 사용했는가?
|
||||
- 실제 재조회 semantics를 검증할 때 `clear()`까지 고려했는가?
|
||||
- 테스트 데이터가 최소하지만 충분한가?
|
||||
- repository method 결과를 중심으로 assertion하고 있는가?
|
||||
- service 정책이나 웹 계층 검증이 섞이지 않았는가?
|
||||
@@ -0,0 +1,167 @@
|
||||
# @SpringBootTest 사용 기준
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 Spring Boot 테스트에서 `@SpringBootTest`를 언제 사용하고, 언제 사용하지 말아야 하는지를 정의한다.
|
||||
|
||||
이 문서의 목표는 다음과 같다.
|
||||
|
||||
- `@SpringBootTest`를 풀 애플리케이션 컨텍스트 통합 테스트 용도로 한정한다
|
||||
- slice test, repository test, 순수 단위 테스트와 역할을 구분한다
|
||||
- `webEnvironment`별 의미를 명확히 나눈다
|
||||
- 느리고 넓은 테스트를 기본값으로 삼지 않도록 기준을 만든다
|
||||
|
||||
Spring Boot는 `@SpringBootTest`가 `SpringApplication`을 통해 테스트용 `ApplicationContext`를 만들며, Boot 기능이 필요할 때 표준 `@ContextConfiguration`의 대안으로 사용할 수 있다고 설명한다. 또한 더 좁은 범위를 위한 여러 `@…Test` slice annotation도 함께 제공한다고 설명한다.
|
||||
|
||||
## 2. 근거 수준
|
||||
|
||||
- Official: Spring Boot / Spring Framework 공식 문서에서 직접 확인되는 내용
|
||||
- Official + Practice: 공식 동작 위에 일반적인 실무 best practice를 결합한 내용
|
||||
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
|
||||
|
||||
이번 문서는 Spring Boot의 Testing Spring Boot Applications, `@SpringBootTest` API 문서, test slices 문서, Spring Framework의 transaction-in-test 및 context caching 문서를 기준으로 작성한다. `@SpringBootTest`의 컨텍스트 로딩 방식, `webEnvironment`, test slice, rollback 주의사항, 컨텍스트 캐시는 모두 공식 문서로 직접 확인 가능하다.
|
||||
|
||||
## 3. 기본 원칙
|
||||
|
||||
### 3.1 @SpringBootTest는 “전체 애플리케이션 조립이 정말 필요한 테스트”에만 사용한다
|
||||
|
||||
`@SpringBootTest`는 Spring Boot 기능이 적용된 실제 애플리케이션 컨텍스트를 띄운다. 따라서 이 어노테이션의 기본 의미는 “빈 하나만 검증”이 아니라 애플리케이션이 Boot 방식으로 조립되었을 때도 기대한 동작이 나오는지 확인하는 것이다. Boot 문서도 `@SpringBootTest`를 Boot features가 필요할 때 사용하는 어노테이션으로 설명하고, 더 구체적인 영역 테스트에는 slice annotation을 사용하라고 안내한다.
|
||||
|
||||
### 3.2 더 좁은 범위로 충분하면 @SpringBootTest를 쓰지 않는다
|
||||
|
||||
Spring Boot는 `@WebMvcTest`, `@JsonTest`, `@DataJpaTest` 같은 slice 테스트를 제공하고, 이들은 애플리케이션의 특정 부분만 auto-configuration 해 준다고 설명한다. 따라서 controller, JSON 직렬화, JPA repository처럼 대상 범위가 좁다면 먼저 slice test를 검토하고, 정말로 여러 레이어와 Boot auto-configuration을 함께 검증해야 할 때만 `@SpringBootTest`를 올린다.
|
||||
|
||||
### 3.3 @SpringBootTest는 느릴 수 있다는 전제를 갖고 쓴다
|
||||
|
||||
Spring TestContext Framework는 `ApplicationContext`를 static cache에 저장해 같은 컨텍스트는 재사용할 수 있게 해 주지만, 컨텍스트 로딩 자체는 여전히 비용이 크고, 컨텍스트 종류가 많아질수록 전체 테스트 시간이 늘어날 수 있다. 따라서 `@SpringBootTest`는 “편하니까 기본값”이 아니라, 컨텍스트 로딩 비용을 감수할 가치가 있는 테스트에만 써야 한다.
|
||||
|
||||
## 4. 언제 @SpringBootTest를 사용하는가
|
||||
|
||||
### 4.1 Boot auto-configuration과 실제 빈 조합을 함께 검증해야 할 때 사용한다
|
||||
|
||||
`@SpringBootTest`는 `SpringApplication`으로 컨텍스트를 만들고, 외부 설정, 로깅, Boot 기능을 기본적으로 적용한 상태를 재현한다. 따라서 실제 `@ConfigurationProperties`, auto-configuration, component scanning, AOP, security chain, application event wiring, 실제 빈 조합을 함께 검증해야 할 때 적합하다.
|
||||
|
||||
### 4.2 애플리케이션이 정상적으로 기동되는지 확인하는 smoke test에 사용한다
|
||||
|
||||
`@SpringBootTest`는 별도 설정이 없으면 `@SpringBootConfiguration`을 자동으로 찾고, Boot 방식으로 컨텍스트를 로딩한다. 따라서 `contextLoads()` 같은 smoke test는 `@SpringBootTest`의 대표적인 사용처다. 이는 “서비스가 실제 배포 구성을 기준으로 부팅 가능한가”를 빠르게 확인하는 최소 통합 테스트다.
|
||||
|
||||
### 4.3 여러 레이어를 한 번에 검증하는 use case 통합 테스트에 사용한다
|
||||
|
||||
컨트롤러-서비스-리포지토리-트랜잭션-AOP-설정 바인딩이 함께 맞물린 동작을 검증해야 한다면 slice test만으로는 부족할 수 있다. 이런 경우 `@SpringBootTest`가 적합하다. 다만 이때도 “한 서비스 메서드 통합”, “보안 포함 MVC 경로 통합”처럼 왜 전체 컨텍스트가 필요한지가 분명해야 한다. 이 해석은 Boot 문서의 full application context 성격과 slice 구분을 바탕으로 한 best practice다.
|
||||
|
||||
## 5. 언제 @SpringBootTest를 사용하지 않는가
|
||||
|
||||
### 5.1 순수 단위 테스트에는 사용하지 않는다
|
||||
|
||||
빈 하나의 로직만 검증하고 Spring 컨테이너가 필요 없다면 `@SpringBootTest`는 과도하다. Spring 문서는 테스트 지원이 통합 테스트에 강점을 가지지만, IoC 덕분에 단위 테스트도 쉽게 할 수 있다고 설명한다. 프로젝트에서는 순수 계산, 도메인 로직, 단일 서비스의 협력 객체 스텁/페이크 테스트에 `@SpringBootTest`를 기본 금지한다.
|
||||
|
||||
### 5.2 repository 전용 테스트에는 기본적으로 사용하지 않는다
|
||||
|
||||
Spring Boot는 `@DataJpaTest` 같은 데이터 접근 slice를 제공하고, JPA 테스트는 엔티티와 repository만 좁게 로딩하도록 설계되어 있다. 따라서 repository 전용 검증에 `@SpringBootTest`를 기본값으로 두면 범위가 과도하다. repository test 기준은 다음 문서에서 따로 상세히 다루지만, 이 문서 수준에서도 repository만 보려는 테스트에 full context는 기본 금지가 맞다.
|
||||
|
||||
### 5.3 MVC 계층만 검증하는 테스트에는 기본적으로 사용하지 않는다
|
||||
|
||||
Spring Boot는 Spring MVC controller 테스트에 `@WebMvcTest`를 제공한다고 설명한다. 따라서 controller request mapping, validation, status code, JSON binding, advice 같은 MVC 계층만 보려는 테스트에 `@SpringBootTest`를 기본값으로 쓰지 않는다. 전체 컨텍스트가 필요한 특별한 이유가 있을 때만 예외적으로 사용한다.
|
||||
|
||||
## 6. webEnvironment 기준
|
||||
|
||||
### 6.1 기본값은 목적에 맞는 가장 좁은 webEnvironment
|
||||
|
||||
Spring Boot는 `@SpringBootTest`의 `webEnvironment`로 `MOCK`, `RANDOM_PORT`, `DEFINED_PORT`, `NONE`을 제공한다고 설명한다. 프로젝트 기본 원칙은 가장 좁은 환경을 먼저 고르는 것이다. 웹 서버가 실제로 필요 없으면 서버를 띄우지 않는다.
|
||||
|
||||
### 6.2 NONE: non-web full context 테스트의 기본값
|
||||
|
||||
`NONE`은 `SpringApplication`으로 `ApplicationContext`를 로드하지만 웹 환경은 만들지 않는다. 따라서 웹 서버가 필요 없는 full-context 테스트, 예를 들어 서비스 통합 테스트나 단순 기동 테스트의 기본값으로 가장 적절하다.
|
||||
|
||||
### 6.3 MOCK: 실제 서버 없이 웹 애플리케이션을 통합 검증할 때 사용한다
|
||||
|
||||
`MOCK`은 기본값이며, web `ApplicationContext`를 로드하지만 내장 서버는 시작하지 않는다. Boot 문서는 이 모드가 `@AutoConfigureMockMvc` 또는 `@AutoConfigureWebTestClient`와 함께 mock 기반 웹 테스트에 적합하다고 설명한다. 프로젝트에서는 보안 필터, MVC 설정, Jackson, 예외 처리까지 포함하되 실제 포트는 필요 없는 경우 `MOCK`을 사용한다.
|
||||
|
||||
### 6.4 RANDOM_PORT: 실제 내장 서버와 실제 HTTP 왕복이 필요할 때 사용한다
|
||||
|
||||
`RANDOM_PORT`는 실제 `WebServerApplicationContext`를 만들고, 임의 포트에 내장 서버를 띄운다. 프로젝트에서는 실제 HTTP stack, 필터 체인, 포트 바인딩, serialization, client-server 상호작용 자체를 검증해야 할 때만 사용한다. 자동화 테스트에서는 `DEFINED_PORT`보다 충돌 위험이 적어 일반적으로 더 안전하다. 앞 문장은 Boot가 `RANDOM_PORT`와 `DEFINED_PORT`를 구분해 설명하는 점 위에 얹는 프로젝트 권장안이다.
|
||||
|
||||
### 6.5 DEFINED_PORT: 예외적 상황에서만 사용한다
|
||||
|
||||
`DEFINED_PORT`는 설정 파일 또는 기본 포트 8080으로 실제 서버를 띄운다. Boot 문서는 이 모드를 공식 지원하지만, 프로젝트에서는 고정 포트를 요구하는 외부 연동 테스트처럼 특별한 이유가 있을 때만 사용한다. 일반 테스트 스위트 기본값으로 두기에는 포트 충돌과 환경 의존성이 커질 수 있다. 이 판단은 Boot의 포트 모드 설명 위에 얹는 best practice다.
|
||||
|
||||
## 7. 트랜잭션 기준
|
||||
|
||||
### 7.1 같은 스레드 안에서 실행되는 @Transactional 테스트는 기본적으로 롤백된다
|
||||
|
||||
Spring TestContext Framework는 테스트 메서드에 `@Transactional`이 붙으면 테스트를 트랜잭션 안에서 실행하고, 기본적으로 종료 시 롤백한다고 설명한다. 따라서 same-thread 방식의 `@SpringBootTest`에서는 테스트 데이터 정리에 유용할 수 있다.
|
||||
|
||||
### 7.2 RANDOM_PORT / DEFINED_PORT에서는 테스트 메서드 롤백을 서버 처리까지 기대하지 않는다
|
||||
|
||||
Spring Boot는 `RANDOM_PORT`나 `DEFINED_PORT`에서는 실제 서버와 클라이언트가 별도 스레드에서 실행되므로, 테스트 메서드의 `@Transactional` 롤백이 서버 쪽 트랜잭션에는 적용되지 않는다고 명시한다. 프로젝트에서는 이 모드에서 “테스트 끝나면 DB가 자동 롤백될 것”이라는 기대를 금지한다.
|
||||
|
||||
## 8. slice 테스트와의 관계
|
||||
|
||||
### 8.1 slice가 가능하면 slice를 우선한다
|
||||
|
||||
Spring Boot는 test slices가 애플리케이션의 특정 부분만 테스트하도록 설계되었다고 설명한다. 프로젝트에서는 `@SpringBootTest`보다 좁은 slice가 정확히 요구사항을 만족하면 slice를 우선한다. full context는 필요 비용이 더 크기 때문이다.
|
||||
|
||||
### 8.2 여러 slice를 동시에 섞지 않는다
|
||||
|
||||
Spring Boot는 여러 `@…Test` slice annotation을 한 테스트에 함께 쓰는 것은 지원하지 않는다고 설명한다. 여러 조각이 동시에 필요하면 하나의 slice를 고르고, 다른 기능은 필요한 `@AutoConfigure…`를 수동으로 더하라고 안내한다. 프로젝트에서도 slice를 여러 개 겹치는 구조는 기본 금지다.
|
||||
|
||||
### 8.3 full context가 필요하지만 테스트 편의 기능도 원하면 @AutoConfigure…를 @SpringBootTest와 조합한다
|
||||
|
||||
Spring Boot는 `@AutoConfigure…` 계열 어노테이션을 `@SpringBootTest`와 조합할 수 있다고 설명한다. 따라서 전체 컨텍스트는 유지하되 `MockMvc`, `WebTestClient` 같은 테스트 편의 빈이 필요하면 이 조합을 사용한다.
|
||||
|
||||
## 9. 컨텍스트 캐시 기준
|
||||
|
||||
### 9.1 @SpringBootTest 변형을 최소화해 컨텍스트 캐시를 재사용한다
|
||||
|
||||
Spring TestContext Framework는 `ApplicationContext`를 static cache에 저장해 재사용한다고 설명한다. 따라서 properties, profiles, 임시 설정 클래스, 불필요한 커스텀 조합을 테스트마다 제각각 바꾸면 캐시 재사용이 줄고 전체 테스트가 느려질 수 있다. 프로젝트에서는 비슷한 목적의 `@SpringBootTest`는 같은 컨텍스트 구성을 공유하도록 정리한다.
|
||||
|
||||
## 10. 프로젝트 권장안
|
||||
|
||||
### 10.1 @SpringBootTest는 테스트 피라미드의 상단에 둔다
|
||||
|
||||
프로젝트 기본 전략은 다음과 같다.
|
||||
|
||||
- 순수 로직은 단위 테스트
|
||||
- 기술 경계별 검증은 slice test
|
||||
- 여러 레이어와 Boot 조립을 함께 확인해야 하는 경우에만 `@SpringBootTest`
|
||||
|
||||
이 규칙은 Spring Boot가 slices와 full-context test를 함께 제공하는 설계와 맞는 프로젝트 권장안이다.
|
||||
|
||||
### 10.2 non-web full-context 테스트의 기본값은 webEnvironment = NONE
|
||||
|
||||
실제 서버가 필요 없는데도 기본 `MOCK`이나 real server 모드를 쓰면 테스트 의도가 흐려질 수 있다. 프로젝트에서는 웹이 아닌 full-context 테스트는 `NONE`을 기본값으로 둔다. 이는 Boot가 `NONE`을 공식 지원한다는 점 위에 얹는 프로젝트 규칙이다.
|
||||
|
||||
### 10.3 웹 통합 테스트는 두 갈래로 나눈다
|
||||
|
||||
프로젝트 권장안은 다음과 같다.
|
||||
|
||||
- 실제 포트가 필요 없고 MVC/보안/직렬화 조합만 보면 된다 → `MOCK` + `@AutoConfigureMockMvc`
|
||||
- 실제 서버와 실제 HTTP round-trip이 필요하다 → `RANDOM_PORT`
|
||||
|
||||
이 구분은 Boot의 `webEnvironment` 설명을 실무적으로 정리한 프로젝트 권장안이다.
|
||||
|
||||
## 11. 금지 규칙
|
||||
|
||||
다음은 기본 금지다.
|
||||
|
||||
- 순수 단위 테스트에 `@SpringBootTest` 사용
|
||||
- repository 전용 테스트에 기본적으로 `@SpringBootTest` 사용
|
||||
- MVC 슬라이스로 충분한데 full context를 올리는 것
|
||||
- `RANDOM_PORT`/`DEFINED_PORT` 테스트에서 `@Transactional` 롤백을 서버 처리까지 기대하는 것
|
||||
- 여러 slice annotation을 한 테스트에 동시에 붙이는 것
|
||||
- 목적 없이 properties, profiles, 설정 클래스를 계속 바꿔 컨텍스트 캐시를 깨는 것
|
||||
- full context가 필요한 이유를 설명하지 못하는 `@SpringBootTest` 추가
|
||||
|
||||
이 금지 규칙은 Spring Boot의 full-context testing/slice 문서와 Spring TestContext의 트랜잭션·컨텍스트 캐시 문서를 바탕으로 한 best practice다.
|
||||
|
||||
## 12. 체크리스트
|
||||
|
||||
다음 질문에 “예”로 답할 수 있어야 한다.
|
||||
|
||||
- 이 테스트는 정말 full application context가 필요한가?
|
||||
- slice test로 줄일 수 없는가?
|
||||
- `webEnvironment`가 목적에 비해 과하지 않은가?
|
||||
- 정렬된 목적에 맞게 `NONE`, `MOCK`, `RANDOM_PORT`, `DEFINED_PORT`를 골랐는가?
|
||||
- `RANDOM_PORT`/`DEFINED_PORT`에서 rollback 기대를 잘못 두고 있지 않은가?
|
||||
- 컨텍스트 구성 변형을 최소화하고 있는가?
|
||||
- 같은 목적의 테스트들이 컨텍스트 캐시를 재사용할 수 있는가?
|
||||
Reference in New Issue
Block a user