Post

@MockitoSpyBean으로 통합 테스트의 순수성 지키기

@MockitoSpyBean으로 통합 테스트의 순수성 지키기

통합 테스트인데 실제 DB를 전혀 검증하지 않았다

도메인 통합 테스트를 작성하던 중 뭔가 이상하다는 생각이 들었습니다. 서비스 레이어 테스트인데 Repository를 mock()으로 처리하고 있었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 변경 전: 이게 정말 통합 테스트인가?
@ExtendWith(MockitoExtension.class)
class PointServiceTest {

    @Mock
    private PointRepository pointRepository;

    @InjectMocks
    private PointService pointService;

    @Test
    void 포인트_충전() {
        when(pointRepository.findByUserId(1L))
            .thenReturn(Optional.of(new Point(1L, 1000)));

        pointService.charge(1L, 500);

        verify(pointRepository).save(any());
    }
}

when().thenReturn()으로 모든 동작을 미리 정의해두니, 실제 JPA 쿼리가 제대로 동작하는지 전혀 검증이 안 됩니다. 이건 단위 테스트입니다. 통합 테스트가 아닙니다.

@MockitoSpyBean이란?

@MockitoSpyBean은 실제 Bean을 Spring Context에서 가져오되, 특정 메서드만 선택적으로 stubbing할 수 있습니다.

핵심은 기본 동작이 “진짜 동작”이라는 점입니다.

구분MockSpy
기본 동작모든 메서드를 가짜로실제 동작
stubbing원하는 메서드를 가짜로 교체원하는 메서드만 가짜로 교체
용도단위 테스트통합 테스트에서 일부만 격리

변경 후: 진짜 통합 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@SpringBootTest
class PointServiceIntegrationTest {

    @Autowired
    private PointService pointService;

    @Autowired
    private PointRepository pointRepository;

    @MockitoSpyBean
    private ExternalNotificationClient notificationClient;

    @Test
    @Transactional
    void 포인트_충전_후_실제_DB에_저장된다() {
        // given
        Point point = pointRepository.save(new Point(1L, 1000));

        // 외부 알림은 실제 호출하지 않고 stubbing
        doNothing().when(notificationClient).sendChargeNotification(any());

        // when
        pointService.charge(1L, 500);

        // then - 실제 DB에서 조회해서 검증
        Point saved = pointRepository.findByUserId(1L).orElseThrow();
        assertThat(saved.getBalance()).isEqualTo(1500);

        // 알림 클라이언트가 호출됐는지 검증
        verify(notificationClient).sendChargeNotification(any());
    }
}

이제 실제 JPA 쿼리가 수행되고, 실제 DB에 저장되는지 검증됩니다. 외부 시스템 연동(알림 클라이언트)만 선택적으로 stubbing했습니다.

인사이트

Spy는 “실제 동작 + 검증 도구”의 조합입니다. 실제 동작을 유지하면서 호출 여부, 호출 횟수, 인자 등을 검증할 수 있습니다.

통합 테스트는 “결과”를 검증합니다. Mock 기반 테스트는 “행동”을 검증합니다. 솔직히 통합 테스트에서 verify만 쓰고 assertThat이 없다면, 그건 테스트 전략이 잘못된 겁니다. 처음 이 코드를 보고 당황했습니다.

테스트 전략 레이어링

테스트는 목적에 따라 계층을 나눠야 합니다.

1
2
3
4
5
6
7
8
9
10
11
단위 테스트 (@Mock + @InjectMocks)
  └── 도메인 로직의 순수 검증
  └── 빠른 피드백

통합 테스트 (@MockitoSpyBean + @SpringBootTest)
  └── 실제 DB 저장/조회 검증
  └── 레이어 간 협력 검증
  └── 외부 의존성만 선택적 격리

E2E 테스트 (TestRestTemplate / MockMvc)
  └── API 레벨 전체 흐름 검증

각 레이어가 검증해야 할 것을 명확히 하고, 그에 맞는 도구를 선택해야 합니다. @MockitoSpyBean은 통합 테스트 레이어에서 외부 의존성을 격리할 때 딱 맞는 도구입니다.

This post is licensed under CC BY 4.0 by the author.