# 단위 테스트 코드 작성 방법

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">이 글은 다음 블로그를 참고하여 작성된 글입니다. <a target="_self" rel="noopener noreferrer nofollow" href="https://mangkyu.tistory.com/143" style="pointer-events: none">https://mangkyu.tistory.com/143</a> <a target="_self" rel="noopener noreferrer nofollow" href="https://tech.kakaopay.com/post/given-test-code-2/" style="pointer-events: none">https://tech.kakaopay.com/post/given-test-code-2/</a></div>
</div>

## 단위 테스트란?

단위 테스트(Unit Test)는  
**<mark>하나의 모듈을 기준으로 독립적으로 수행되는 가장 작은 단위의 테스트</mark>**이다.

여기서 말하는 *모듈*이란,  
애플리케이션에서 동작하는 **<mark>하나의 기능 또는 하나의 메서드</mark>**를 의미한다.

---

## 단위 테스트의 어려움

일반적인 애플리케이션에서는  
하나의 기능을 처리하기 위해 여러 객체와 메시지를 주고받는다.

하지만 단위 테스트는 특정 모듈을 **독립적으로 검증**하는 것이 목적이기 때문에,  
다른 객체와의 협력이 포함되면 테스트가 어려워진다.

이 문제를 해결하기 위해,  
단위 테스트에서는 실제 객체 대신 **<mark>가짜 객체(Mock Object)</mark>**를 주입하고  
“어떤 상황에서 어떤 결과를 반환할지”를 미리 정의한다.

이렇게 미리 정해진 응답을 준비하는 과정을 **Stub**이라고 한다.

---

## 좋은 단위 테스트의 특징

좋은 단위 테스트는 다음 원칙을 따른다.

* 하나의 테스트 함수는 **하나의 개념만 검증한다**
    
* 테스트 함수 내에서 **assert는 최소화한다**
    

또한, 좋은 테스트 코드는 흔히 **FIRST 원칙**을 만족해야 한다.

* **Fast**  
    테스트는 빠르게 실행되어 자주 돌릴 수 있어야 한다.
    
* **Independent**  
    각 테스트는 서로 의존하지 않고 독립적으로 실행되어야 한다.
    
* **Repeatable**  
    어떤 환경에서도 동일한 결과를 보장해야 한다.
    
* **Self-Validating**  
    성공 또는 실패가 명확해야 하며, 추가 해석이 필요 없어야 한다.
    
* **Timely**  
    실제 코드를 작성하기 직전 또는 동시에 작성되어야 한다.
    

---

## Given / When / Then 패턴

단위 테스트에서는  
**테스트의 의도를 코드만 보고도 이해할 수 있도록** 만드는 것이 중요하다.

이를 위해 가장 널리 사용되는 구조가  
**Given / When / Then 패턴**이다.

* **Given**: 테스트를 위한 사전 조건을 준비한다.
    
* **When**: 테스트 대상의 동작을 실행한다.
    
* **Then**: 실행 결과를 검증한다.
    

이 패턴의 목적은

> *“이 테스트가 무엇을 검증하는지”*  
> 를 코드 구조만으로 드러내는 데 있다.

---

## 외부 의존성이 없는 경우: 순수 객체 기반 테스트

로또 번호 생성기 예제처럼  
외부 의존성이 전혀 없는 경우에는 매우 단순한 테스트 구조를 유지할 수 있다.

```plaintext
// given
final LottoNumberGenerator lottoNumberGenerator = new LottoNumberGenerator();
final int price = 1000;

// when
final List<Integer> lotto = lottoNumberGenerator.generate(price);

// then
assertThat(lotto).hasSize(6);
```

이 경우 테스트 대상은 오직 하나의 객체이며,  
`new` 키워드만으로 충분히 단위 테스트를 구성할 수 있다.

---

## 왜 예약 도메인에서는 같은 방식이 어려울까?

예약 도메인의 서비스 로직은  
로또 예제와 구조적으로 다르다.

하나의 기능을 수행하기 위해  
여러 협력 객체와 메시지를 주고받기 때문이다.

```plaintext
photoLabRepository.findById(...)
memberUserRepository.findById(...)
reservationSlotRepository.findByPhotoLabIdAndReservationDateAndReservationTimeForUpdate(...)
reservationRepository.save(...)
```

이 서비스는 다음과 같은 특징을 가진다.

* Repository를 통해 DB에 접근한다.
    
* 트랜잭션과 락 조회가 포함된다.
    
* 여러 협력 객체의 상태에 따라 분기 로직이 달라진다.
    

이런 구조에서는 단순히 객체를 `new` 해서 테스트를 실행할 수 없다.  
테스트가 DB 상태, 트랜잭션 환경, 외부 설정에 영향을 받게 되기 때문이다.

즉, **Mock 없이 실제 협력 객체만으로는**  
순수 객체 기반 단위 테스트를 구성하기 어렵다.

---

## 그래서 단위 테스트에서는 Mock을 사용한다

이러한 문제를 해결하기 위해,  
단위 테스트에서는 실제 객체 대신 **행동이 고정된 가짜 객체(Mock)**를 주입한다.

중요한 점은,  
Mock을 사용하더라도 **테스트의 초점은 여전히 Service에 있다는 것**이다.

---

## Mock을 사용했지만, 테스트의 초점은 Service에 있다

아래 테스트는  
`ReservationCommandService` **하나의 개념만**을 검증한다.

```plaintext
@ExtendWith(MockitoExtension.class)
class ReservationCommandServiceImplTest {

    @Mock PhotoLabRepository photoLabRepository;
    @Mock MemberUserRepository memberUserRepository;
    @Mock ReservationRepository reservationRepository;
    @Mock ReservationSlotRepository reservationSlotRepository;

    @InjectMocks
    ReservationCommandServiceImpl service;
}
```

모든 Repository는 Mock으로 대체되었고,  
각각의 반환 값은 **Given 단계에서 명확하게 정의**된다.

```plaintext
given(photoLabRepository.findById(labId))
        .willReturn(Optional.of(lab));

given(memberUserRepository.findById(memberId))
        .willReturn(Optional.of(user));
```

이를 통해 이 테스트는

* DB 동작을 검증하지 않고
    
* 트랜잭션 구현을 신경 쓰지 않으며
    
* 오직  
    \*\*“이 서비스가 주어진 상황에서 올바른 결정을 내리는지”\*\*만 확인한다.
    

---

## Given 지옥을 피하기 위해 적용한 방법

협력 객체가 많은 서비스 테스트에서는  
Given 코드가 길어지기 쉽다.

이를 방치하면 테스트의 의도가 흐려진다.

### 1\. Fixture로 객체 생성 책임을 분리했다

```plaintext
var lab = ReservationDomainFixture.photoLab(labId);
var user = ReservationDomainFixture.memberUser(memberId);
var slot = ReservationDomainFixture.slot(lab, 100L, date, time);
```

테스트에서는  
**무엇을 검증하는지만 드러나야 한다.**

객체 생성 방식은 Fixture로 숨긴다.

---

### 2\. 결과 중심으로 검증했다

```plaintext
assertThat(response.reservationId()).isEqualTo(777L);
```

내부 구현이 아니라  
**최종 결과와 핵심 부작용만 검증**한다.

---

### 3\. 한 테스트는 하나의 개념만 검증했다

```plaintext
void createReservation_정상_슬롯없으면_생성후_예약저장()
```

* 메서드 이름 자체가 시나리오
    
* 실패 원인이 즉시 드러남
    
* 불필요한 assert 제거
    

---

## Spring Context를 띄우지 않은 이유

이 테스트는 `@SpringBootTest`를 사용하지 않는다.  
즉, **Spring Application Context를 띄우지 않는다.**

그 결과 다음과 같은 장점이 있다.

* 테스트 실행 속도가 빠르다 (**Fast**)
    
* 테스트 간 상태 공유가 없다 (**Independent**)
    
* 환경에 관계없이 반복 가능하다 (**Repeatable**)
    

이는 FIRST 원칙을 충족하는  
**단위 테스트에 가까운 구조**이다.

---

## 정리

* 단위 테스트는 “Mock이 없는 테스트”가 아니다.
    
* 단위 테스트의 핵심은  
    **하나의 개념을 외부 영향 없이 검증하는 것**이다.
    
* 외부 환경에 의해 결과가 달라질 수 있는 의존성만 <mark>Mock으로 대체</mark>한다.
    
* Given / When / Then 구조를 통해 테스트의 의도를 드러낸다.
    
* Fixture와 명확한 네이밍으로 Given 지옥을 줄인다.
    

이러한 기준을 지키면,  
협력 객체가 많은 서비스 로직에서도  
**읽기 쉽고 신뢰할 수 있는 단위 테스트**를 작성할 수 있다.
