면접에서 테스트에 대해
진득하게 털리고차근차근 공부하는 테스트🥲 다음엔 털리지 말자
@ExtendWith(MockitoExtension.class)
public class PointServiceTest {
@InjectMocks PointService pointService; // Mock을 주입받은 테스트 대상
@Mock PointAccountRepository pointAccountRepository; // 가짜 Repository
@Test
void 신규계정_잔액은_0원(){
//given
String userId = "user1";
given(pointAccountRepository.save(any()))
.willAnswer(invocation -> invocation.getArgument(0)); //"save() 메서드가 호출되면, 첫 번째 인자(0번 인덱스)를 그대로 반환해줘"
//when
var account = pointService.createAccount(userId);
//then
assertThat(account.getBalance()).isZero();
}
}
// Mock 없는 테스트
@Test
void 실제객체_사용() {
// 실제 DB 연결이 필요함
PointAccountRepository realRepository = new JpaPointAccountRepository();
PointService pointService = new PointService(realRepository);
var acc = pointService.createAccount("user1"); // 실제 DB 접근
}
발생하는 문제들:
내가 계속 혼동하는 부분:
// 1. 객체 생성 - DB 연결 하지 않음
PointAccountRepository realRepository = new JpaPointAccountRepository();
// ↑ 이 시점에는 DB 연결 없음. 그냥 객체만 생성
// 2. 메서드 호출 - 여기서 DB 연결!
realRepository.save(account);
// ↑ 이 시점에서 실제 DB에 INSERT 쿼리 실행
// Real Repository 동작
pointService.createAccount("user1") {
1. new PointAccount("user1") 생성
2. repository.save() 호출
→ DB 연결
→ INSERT 쿼리 실행
→ 네트워크 통신
→ 트랜잭션 처리
→ 결과 반환 (느림, 복잡함)
}
// Mock Repository 동작
pointService.createAccount("user1") {
// 1단계: PointAccount 객체 생성
PointAccount account = new PointAccount("user1", 0L);
// 메모리에 실제 객체 생성됨
// account = PointAccount { userId: "user1", balance: 0 }
// 2단계: repository.save() 호출
return pointAccountRepository.save(account);
// ↑
// 이 account 객체를 인자로 넘김
// 3단계: Mock이 willAnswer 규칙 적용
// invocation.getArgument(0) = 넘어온 첫 번째 인자 = account 객체
// 즉, 넘어온 account 객체를 그대로 반환
}
🔴 Red → 🟢 Green → 🔵 Blue(Refactor)
Step 1: Red - 실패하는 테스트 작성
@Test
void 신규계정_잔액은_0원(){
//given
String userId = "user1";
//when
var account = pointService.createAccount(userId); // 컴파일 에러!
//then
assertThat(account.getBalance()).isZero();
}
결과: PointService.createAccount() 메서드가 없거나 null을 반환해서 에러를 만든다. 지금 경우는 그냥 없음.
Step 2: Green - 최소한의 코드로 통과
public class PointService {
public PointAccount createAccount(String userId) {
return new PointAccount(userId, 0L); // 하드코딩으로 통과
}
}
결과: 테스트 통과! (하지만 하드코딩)
Step 3: Blue - 리팩토링 (Mock 추가)
public class PointService {
private final PointAccountRepository repository;
public PointAccount createAccount(String userId) {
PointAccount account = new PointAccount(userId, 0L);
return repository.save(account); // Repository 패턴 적용
}
}
// 테스트에 Mock 추가
@Test
void 신규계정_잔액은_0원(){
//given
String userId = "user1";
given(pointAccountRepository.save(any()))
.willAnswer(invocation -> invocation.getArgument(0));
//when
var account = pointService.createAccount(userId);
//then
assertThat(account.getBalance()).isZero();
assertThat(account.getUserId()).isEqualTo(userId);
}
Step 3: Blue - 리팩토링 (Mock 추가)
public class PointService {
private final PointAccountRepository repository;
public PointAccount createAccount(String userId) {
PointAccount account = new PointAccount(userId, 0L);
return repository.save(account); // Repository 패턴 적용
}
}
// 테스트에 Mock 추가
public class PointService {
private final PointAccountRepository repository;
public PointAccount createAccount(String userId) {
PointAccount account = new PointAccount(userId, 0L);
return repository.save(account); // Repository 패턴 적용
}
}
// 테스트에 Mock 추가
@Test
void 신규계정_잔액은_0원(){
//given
String userId = "user1";
given(pointAccountRepository.save(any()))
.willAnswer(invocation -> invocation.getArgument(0));
//when
var account = pointService.createAccount(userId);
//then
assertThat(account.getBalance()).isZero();
assertThat(account.getUserId()).isEqualTo(userId);
}
@Test
void Mock설정_없는_잘못된_테스트(){
//given
String userId = "user1";
// ← Mock 설정이 없음!
//when
var account = pointService.createAccount(userId);
// ↑ pointAccountRepository.save()가 null 반환!
//then
assertThat(account.getBalance()).isZero(); // NullPointerException 발생!
}
//given
String userId = "user1"; // ← 실제 테스트 데이터 준비
given(pointAccountRepository.save(any())) // ← Mock 동작 정의 (인프라 준비)
.willAnswer(invocation -> invocation.getArgument(0));
구분해서 이해해보자:
@BeforeEach
void setUp() {
// Mock 동작을 여기서 미리 설정 (인프라 준비)
given(pointAccountRepository.save(any(PointAccount.class)))
.willAnswer(invocation -> invocation.getArgument(0));
}
@Test
void 신규계정_잔액은_0원(){
//given - 순수한 테스트 데이터만
String userId = "user1";
//when
var account = pointService.createAccount(userId);
//then
assertThat(account.getBalance()).isZero();
assertThat(account.getUserId()).isEqualTo(userId);
// 추가 검증: repository의 save가 실제로 호출되었는지
verify(pointAccountRepository).save(any(PointAccount.class));
}
@Test
void 신규계정_잔액은_0원(){
// 우리의 관심사: "PointService가 새 계정을 올바르게 생성하는가?"
// 관심 없는 것: "Repository가 DB에 잘 저장하는가?"
// ↑ 이건 Repository 테스트에서 별도로 검증!
var acc = pointService.createAccount("user1");
assertThat(acc.getBalance()).isZero(); // 이것만 검증하고 싶음
}
내가 테스트하고 싶은 로직에만 집중할 수 있게 해준다
이게 면접에서 나왔고 테스트는 mock을 사용한 테스트 밖에 모른다고 멍청한 대답을 당당히 하고 나왔다...🤦🏻♀️
🏭 Detroit School (Classicist) - "Mock은 가짜 테스트다!"
핵심 철학: "실제 객체들이 상호작용해야 의미 있는 테스트"
주요 주장:
// Detroit School 방식
@Test
void detroit_방식() {
Calculator realCalc = new Calculator(); // 실제 객체
TaxService realTax = new TaxService(); // 실제 객체
OrderService order = new OrderService(realCalc, realTax);
// 실제 메서드들이 실제로 호출되고 계산됨
assertEquals(110, order.calculateTotal(100));
}
🏢 London School (Mockist) - "Mock이 진짜 단위테스트다!"
핵심 철학: "의존성 있으면 단위테스트가 아니다"
주요 주장:
// London School 방식
@Test
void london_방식() {
Calculator mockCalc = mock(Calculator.class);
TaxService mockTax = mock(TaxService.class);
when(mockCalc.calculate(100)).thenReturn(100);
when(mockTax.getTax(100)).thenReturn(10);
OrderService order = new OrderService(mockCalc, mockTax);
assertEquals(110, order.calculateTotal(100)); // 완벽히 격리된 테스트
}
Detroit School (실제 객체)
장점:
단점:
London School (Mock)
장점:
단점:
void 이메일발송_테스트(){
// 실제 이메일 서버에 보낼 수 없으니까 Mock 필수
EmailService mockEmail = mock(EmailService.class);
UserService service = new UserService(mockEmail);
service.registerUser("test@test.com");
verify(mockEmail).send("test@test.com", "Welcome!");
}
@Test
void 데이터베이스_저장_테스트() {
// 실제 DB 연결하면 너무 느려서 Mock 사용
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
service.saveUser(new User("test"));
verify(mockRepo).save(any(User.class));
}
@Test
void 네트워크_장애_테스트() {
// 실제로 네트워크 장애 만들기 어려우니까 Mock으로 시뮬레이션
ApiClient mockApi = mock(ApiClient.class);
when(mockApi.call()).thenThrow(new NetworkException());
PaymentService service = new PaymentService(mockApi);
assertThrows(PaymentFailedException.class,
() -> service.processPayment(100));
}
@Test
void 금액_계산_테스트() {
// Money, Price 같은 값 객체는 실제 객체 사용
Money price = new Money(1000);
Money tax = new Money(100);
Money total = price.add(tax);
assertThat(total.getAmount()).isEqualTo(1100);
}
@Test
void 계산기_테스트() {
// 간단한 계산기는 실제 객체가 더 의미있음
Calculator calc = new Calculator();
MathService service = new MathService(calc);
int result = service.calculate(10, 5);
assertThat(result).isEqualTo(15);
}
@Test
void 주문_비즈니스_로직_테스트() {
// 핵심 도메인 로직은 실제 객체들의 협력이 중요
PriceCalculator priceCalc = new PriceCalculator();
DiscountPolicy discountPolicy = new VipDiscountPolicy();
OrderService service = new OrderService(priceCalc, discountPolicy);
Order order = service.createOrder("VIP고객", 10000);
assertThat(order.getFinalPrice()).isEqualTo(9000); // 10% 할인
}
결론: 적절히 같이 쓰자 🫶🏻