
요구사항:상품 등록은 '영업 시간(09:00 ~ 18:00)'에만 가능하다.
package com.example.tdd_study.product;
import java.time.LocalDateTime;
public class BadProductService {
public void register(String name, int price) {
// ... (생략)
// [문제점] 메서드 내부에서 '현재 시간'을 직접 생성하고 있습니다.
// 이러면 테스트를 실행하는 시간에 따라 결과가 달라집니다.
// 밤에 테스트를 돌리면 무조건 실패하게 됩니다!
LocalDateTime now = LocalDateTime.now();
if (now.getHour() < 9 || now.getHour() >= 18) {
throw new IllegalStateException("영업 시간이 아닙니다.");
}
// ... (저장 로직)
}
}ProductService의 register 메서드가 현재 시간을 파라미터로 받도록 수정합니다.package com.example.tdd_study.product;
import com.example.tdd_study.notification.NotificationCenter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service
public class ProductService {
private final ProductRepository productRepository;
private final NotificationCenter notificationCenter;
public ProductService(ProductRepository productRepository,
NotificationCenter notificationCenter) {
this.productRepository = productRepository;
this.notificationCenter = notificationCenter;
}
@Transactional
// [변경] LocalDateTime을 파라미터로 받도록 변경 (의존성 주입)
public void register(String name, int price, LocalDateTime now) {
Product product = new Product(name, price);
// [검증] 이제 테스트 코드에서 원하는 시간을 마음대로 넣을 수 있습니다.
if (now.getHour() < 9 || now.getHour() >= 18) {
throw new IllegalStateException("영업 시간이 아닙니다.");
}
productRepository.save(product);
notificationCenter.send("새로운 상품이 등록되었습니다: " + name);
}
}package com.example.tdd_study.product;
import com.example.tdd_study.product.dto.AddProductRequest;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
@RestController
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping("/products")
public void register(@RequestBody @Valid AddProductRequest request) {
// [변경] Controller가 현재 시간을 생성해서 Service에 넘겨줍니다.
productService.register(request.getName(), request.getPrice(), LocalDateTime.now());
}
}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.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
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
private NotificationCenter notificationCenter;
@InjectMocks
private ProductService productService;
@Test
void 영업시간_내에는_상품_등록이_가능하다() {
// given
String name = "아메리카노";
int price = 4000;
// [핵심] 테스트를 위해 '낮 12시'라는 고정된 시간을 만듭니다.
LocalDateTime fixedTime = LocalDateTime.of(2025, 1, 1, 12, 0);
// when
productService.register(name, price, fixedTime);
// then
verify(productRepository).save(any(Product.class));
}
@Test
void 영업시간_외에는_상품_등록이_불가능하다() {
// given
String name = "아메리카노";
int price = 4000;
// [핵심] 테스트를 위해 '새벽 1시'라는 고정된 시간을 만듭니다.
LocalDateTime fixedTime = LocalDateTime.of(2025, 1, 1, 1, 0);
// when & then: 예외가 발생하는지 검증
assertThatThrownBy(() -> productService.register(name, price, fixedTime))
.isInstanceOf(IllegalStateException.class)
.hasMessage("영업 시간이 아닙니다.");
}
}ProductServiceTest 클래스 옆의 재생 버튼(Run)을 클릭합니다.
verify 검증 통과.IllegalStateException이 정상적으로 발생했음을 확인.package com.example.tdd_study.product;
import com.example.tdd_study.product.dto.AddProductRequest;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers; // 로그 출력용
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.time.LocalDateTime; // 시간 클래스 import
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
void 상품_등록_성공() throws Exception {
// given
AddProductRequest request = new AddProductRequest("아메리카노", 4000);
Gson gson = new Gson();
String jsonContent = gson.toJson(request);
// when & then
mockMvc.perform(MockMvcRequestBuilders.post("/products")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonContent))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print()); // [로그] 요청/응답 내용 출력
// [핵심] Controller가 Service에 어떤 파라미터(시간 포함)를 넘기는지 검증
// Controller 내부에서 LocalDateTime.now()를 쓰므로, 정확한 시간보다는
// 'LocalDateTime 타입이 넘어갔는지'를 확인합니다.
verify(productService).register(anyString(), anyInt(), any(LocalDateTime.class));
}
}new, static method(예: now())를 사용하면 테스트가 외부 환경에 휘둘리게 됩니다.다음 시간에는기능 확장 시 TDD 적용 흐름을 통해, 이미 완성된 코드에 새로운 요구사항을 얹을 때 TDD가 얼마나 강력한지 체험해 보겠습니다.