테스트는 보통 버그를 막기 위한 도구로 여겨진다. 물론 맞는 말이다. 테스트는 코드가 기대한 대로 동작하는지 확인하고, 변경이 기존 기능을 깨뜨리지 않도록 돕는다.
하지만 DDD에서 테스트는 그 이상의 역할을 할 수 있다. 좋은 도메인 테스트는 도메인 규칙을 문서화한다. 테스트 이름과 시나리오를 읽는 것만으로도 이 도메인이 어떤 규칙을 갖는지 이해할 수 있다.
DDD가 유비쿼터스 언어를 중요하게 본다면, 테스트도 그 언어를 사용하는 중요한 장소다. 회의에서 쓰는 말, 코드의 메서드 이름, 테스트의 시나리오가 같은 언어를 사용할 때 모델은 더 단단해진다.
테스트 이름은 도메인 문장이 될 수 있다
구독 서비스의 테스트를 생각해보자. 다음과 같은 테스트 이름은 기술적이다.
cancel_success
changePlan_fail
renew_test
무엇을 검증하는지 대략 알 수는 있지만 도메인 규칙은 잘 드러나지 않는다.
반면 다음 이름은 도메인 문장에 가깝다.
만료된_구독은_요금제를_변경할_수_없다
무료_체험을_이미_사용한_회원은_다시_무료_체험을_시작할_수_없다
결제에_실패하면_구독은_유예_기간에_들어간다
해지된_구독은_다시_해지할_수_없다
이런 테스트 이름은 단순한 검증 이름이 아니다. 도메인 규칙을 말한다. 새로 합류한 개발자가 테스트 목록을 보는 것만으로도 중요한 정책을 이해할 수 있다.
좋은 테스트 이름은 구현 방법보다 도메인 규칙을 드러내야 한다.
Given-When-Then은 도메인 시나리오를 만든다
도메인 테스트는 Given-When-Then 구조와 잘 맞는다.
Given: 어떤 도메인 상황이 주어졌을 때
When: 어떤 도메인 행위가 일어나면
Then: 어떤 결과가 되어야 한다
예를 들어 구독 해지를 테스트할 수 있다.
@Test
void 결제_후_7일_이내이고_사용_이력이_없으면_해지_시_전액_환불_대상이_된다() {
// given
Subscription subscription = SubscriptionFixture.activePaidSubscription()
.paidAt(daysAgo(3))
.withoutUsage()
.build();
// when
RefundDecision decision = refundPolicy.decide(subscription, now());
// then
assertThat(decision.isFullRefund()).isTrue();
}
이 테스트는 단순히 메서드 반환값을 확인하는 것이 아니다. 환불 정책의 한 문장을 코드로 표현한다.
Given-When-Then을 잘 쓰면 테스트는 도메인 전문가와도 대화 가능한 형태가 된다. “결제 후 7일 이내이고 사용 이력이 없으면 전액 환불이 맞나요?”라고 질문할 수 있기 때문이다.
도메인 모델 테스트는 빠르고 선명해야 한다
도메인 모델 테스트는 가능한 한 빠르고 선명해야 한다. 데이터베이스, 웹 서버, 외부 API 없이도 실행될 수 있어야 한다.
예를 들어 Subscription의 상태 전이를 테스트하는 데 실제 DB가 필요하지는 않다.
@Test
void 만료일이_지나면_구독은_만료될_수_있다() {
Subscription subscription = SubscriptionFixture.active()
.period(endedYesterday())
.build();
subscription.expire(now());
assertThat(subscription.status()).isEqualTo(EXPIRED);
}
이 테스트는 도메인 객체의 규칙을 직접 검증한다. 빠르게 실행되고, 실패했을 때 원인을 찾기 쉽다.
도메인 테스트가 느리고 복잡하면 개발자는 잘 실행하지 않게 된다. 도메인 규칙은 자주 바뀌므로, 그 규칙을 검증하는 테스트는 가벼워야 한다.
Application Service 테스트와 Domain Model 테스트는 다르다
Application Service 테스트는 유스케이스 흐름을 검증한다. 어떤 Repository를 조회하고, 도메인 객체를 호출하고, 저장하고, 이벤트를 발행하는지 확인한다.
도메인 모델 테스트는 규칙 자체를 검증한다. 구독이 해지 가능한지, 요금제 변경 정책이 어떤 결과를 내는지, 무료 체험 가능 여부가 어떻게 판단되는지 확인한다.
둘을 구분하는 것이 중요하다.
Domain Model Test
- 만료된 구독은 요금제를 변경할 수 없다.
- 결제 실패 시 유예 기간으로 전환된다.
- 무료 체험은 한 번만 가능하다.
Application Service Test
- 요금제 변경 요청 시 구독을 조회하고 변경한 뒤 저장한다.
- 구독 갱신 성공 후 SubscriptionRenewed 이벤트를 발행한다.
- 해지 요청 시 권한 회수 후속 처리를 요청한다.
도메인 규칙을 모두 Application Service 테스트로만 검증하면 테스트가 무거워지고, 규칙의 위치도 흐려진다. 반대로 유스케이스 흐름을 도메인 객체 테스트로만 검증할 수도 없다.
테스트도 책임을 나누어야 한다.
테스트는 유비쿼터스 언어를 강화한다
테스트 코드에는 많은 이름이 등장한다. 테스트 이름, Fixture 이름, 메서드 이름, 변수 이름, Assertion 메시지까지 모두 언어의 일부다.
Subscription subscription = activePaidSubscription();
CancelReason reason = CancelReason.byMember("더 이상 사용하지 않음");
subscription.cancelByMember(reason, now);
이 코드는 도메인 언어를 반복한다. 테스트가 많아질수록 그 언어는 팀 안에 자리 잡는다.
반대로 테스트가 기술적 이름만 사용하면 유비쿼터스 언어를 강화하지 못한다.
Subscription s = fixture1();
s.updateStatus(2);
assertThat(s.getStatus()).isEqualTo(3);
이 테스트는 동작을 검증할 수는 있지만 도메인을 설명하지는 못한다.
좋은 테스트는 실패했을 때도 도메인 문장으로 실패한다.
무료_체험을_이미_사용한_회원은_다시_무료_체험을_시작할_수_없다
이 문장이 깨졌다면 구현뿐 아니라 정책 자체를 다시 확인해야 한다.
Fixture도 도메인 언어로 만들기
도메인 테스트에서 Fixture는 중요하다. 테스트 데이터를 만드는 코드가 복잡하면 테스트의 의도가 흐려진다.
나쁜 Fixture는 도메인 의미보다 필드 값을 나열한다.
new Subscription(id, memberId, planId, "A", start, end, null, false);
좋은 Fixture는 도메인 상황을 만든다.
Subscription subscription = SubscriptionFixture.activePaidSubscription()
.withPlan(PRO)
.paidAt(daysAgo(3))
.withoutUsage()
.build();
이 코드는 “3일 전에 결제했고 사용 이력이 없는 활성 유료 구독”이라는 상황을 만든다. 테스트의 Given이 도메인 문장처럼 읽힌다.
Fixture는 단순 편의 코드가 아니다. 테스트에서 도메인 상황을 표현하는 언어다.
테스트가 설계를 밀어준다
도메인 테스트를 작성하다 보면 설계의 문제도 드러난다.
테스트하기 위해 너무 많은 Mock이 필요하다면 도메인 객체가 외부 의존성을 많이 알고 있을 수 있다. 하나의 규칙을 테스트하기 위해 Application Service 전체를 띄워야 한다면 규칙이 올바른 위치에 있지 않을 수 있다. 테스트 이름은 명확한데 코드가 그 이름을 표현하지 못한다면 메서드 이름이나 모델 구조를 다시 봐야 한다.
예를 들어 다음 테스트를 쓰고 싶은데,
만료된_구독은_요금제를_변경할_수_없다
실제 코드는 여러 서비스와 Repository, 외부 API Mock을 거쳐야만 검증된다면, 이 규칙이 너무 바깥에 흩어져 있는 것일 수 있다.
좋은 도메인 모델은 중요한 규칙을 직접 테스트하기 쉽다.
마치며
테스트는 버그를 막기 위한 도구다. 하지만 DDD에서는 테스트가 도메인 모델을 설명하는 문서가 될 수도 있다.
좋은 도메인 테스트는 구현 세부사항보다 도메인 규칙을 말한다. 테스트 이름은 도메인 문장이 되고, Given-When-Then은 도메인 시나리오가 되며, Fixture는 도메인 상황을 만드는 언어가 된다.
테스트가 유비쿼터스 언어를 사용하면, 코드와 문서와 대화가 가까워진다. 새로 들어온 개발자는 테스트를 읽으며 도메인을 배울 수 있고, 기존 개발자는 정책 변경이 어떤 규칙을 깨뜨리는지 빨리 알 수 있다.
DDD에서 테스트는 단순한 안전망이 아니다. 테스트는 도메인 지식이 코드 안에 살아 있는지 확인하는 가장 구체적인 방법 중 하나다.