
할인 정책 추가상품 가격이 30,000원 이상이면, 1,000원을 자동으로 할인해 주세요. (예: 35,000원 입력 -> 34,000원으로 저장)
if 문을 넣을 수도 있지만, 우리는 도메인 중심 설계를 배웠으므로 Product 객체를 테스트하는 것입니다.
아직 할인 로직은 없지만, 할인이 적용되어야 한다고 우기는 테스트를 먼저 짭니다.package com.example.tdd_study.product;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ProductTest {
@Test
void 가격이_3만원_이상이나면_1000원_할인된다() {
// given
String name = "비싼 커피";
int originalPrice = 35000;
int expectedPrice = 34000; // 1000원 할인 기대
System.out.println("[Test Start] 할인 정책 테스트 시작");
System.out.println("입력 가격: " + originalPrice);
// when
Product product = new Product(name, originalPrice);
// then
System.out.println("검증 결과: 실제 저장된 가격 = " + product.getPrice());
assertThat(product.getPrice()).isEqualTo(expectedPrice);
System.out.println("[Test Pass] 테스트 성공! 할인이 정상 적용됨");
}
@Test
void 가격이_3만원_미만이면_할인이_없다() {
// given
int price = 20000;
// when
Product product = new Product("일반 커피", price);
// then
assertThat(product.getPrice()).isEqualTo(price);
}
}35000원이 그대로 저장되기 때문입니다.
콘솔에는 검증 결과: 실제 저장된 가격 = 35000이라고 찍히며 기대값(34000)과 다르다고 에러가 날 것입니다.
Product 클래스를 수정합니다.
생성자 내부에서 가격을 판단하여 할인을 적용합니다.package com.example.tdd_study.product;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.util.Assert;
@Entity
@Table(name = "products")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int price;
public Product(String name, int price) {
Assert.hasText(name, "상품명은 필수입니다.");
Assert.isTrue(price > 0, "상품 가격은 0보다 커야 합니다.");
Assert.isTrue(price % 1000 == 0, "상품 가격은 1000원 단위여야 합니다.");
// [추가] 할인 정책 적용
if (price >= 30000) {
System.out.println("[Logic] 3만원 이상 감지! 1000원 할인 적용함.");
price = price - 1000;
}
this.name = name;
this.price = price;
}
}ProductTest를 실행해 봅니다.
[Test Start] 할인 정책 테스트 시작
입력 가격: 35000
[Logic] 3만원 이상 감지! 1000원 할인 적용함.
검증 결과: 실제 저장된 가격 = 34000
[Test Pass] 테스트 성공! 할인이 정상 적용됨package com.example.tdd_study.product;
import com.example.tdd_study.notification.NotificationCenter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@Mock // [중요] 이 부분이 없어서 NullPointerException이 발생했습니다.
private NotificationCenter notificationCenter;
@InjectMocks
private ProductService productService;
@Test
void 상품_등록시_고가_상품은_할인되어_저장된다() {
// given
String name = "고급 시계";
int inputPrice = 40000; // 3만원 이상
LocalDateTime fixedTime = LocalDateTime.of(2025, 1, 1, 12, 0);
System.out.println(">>> Service 테스트 시작: " + name + ", 가격: " + inputPrice);
// when
productService.register(name, inputPrice, fixedTime);
// then
// Repository.save()가 호출될 때, 넘겨진 Product 객체를 낚아챕니다(Capture).
ArgumentCaptor<Product> productCaptor = ArgumentCaptor.forClass(Product.class);
verify(productRepository).save(productCaptor.capture());
// [추가] 알림 발송도 검증 (여기서 NPE가 났었음)
verify(notificationCenter).send(any(String.class));
Product savedProduct = productCaptor.getValue();
System.out.println(">>> Repository에 저장된 가격: " + savedProduct.getPrice());
// 40000원이 아닌 39000원이 저장되어야 정상 (1000원 할인)
assertThat(savedProduct.getPrice()).isEqualTo(39000);
System.out.println(">>> Service 테스트 통과 완료");
}
// 기존 테스트 (영업시간 관련)도 함께 유지해야 합니다.
@Test
void 영업시간_외에는_상품_등록이_불가능하다() {
// given
String name = "아메리카노";
int price = 4000;
LocalDateTime fixedTime = LocalDateTime.of(2025, 1, 1, 1, 0); // 새벽 1시
System.out.println(">>> 영업시간 외 테스트 시작 (시간: " + fixedTime + ")");
// when & then
assertThatThrownBy(() -> productService.register(name, price, fixedTime))
.isInstanceOf(IllegalStateException.class)
.hasMessage("영업 시간이 아닙니다.");
System.out.println(">>> 예외 발생 확인 완료");
}
}
>>> Service 테스트 시작: 고급 시계, 가격: 40000
[Logic] 3만원 이상 감지! 1000원 할인 적용함.
>>> Repository에 저장된 가격: 39000
>>> Service 테스트 통과 완료System.out.println을 적절히 사용하여 테스트의 시작, 로직 수행, 검증 결과를 눈으로 확인했습니다.Product의 가격)를 꺼내서 검증했습니다.다음 시간에는단위 테스트와 통합 테스트의 역할을 비교하며, 언제 어떤 테스트를 짜야 효율적인지 정리하는 시간을 갖겠습니다.