Spring Boot/스프링 부트 핵심 가이드
[SpringBoot] 7.4.7 서비스 테스트
nayonsoso
2023. 9. 29. 19:25
Summary :
서비스 테스트는
given - 레포지토리의 메소드가 특정 값을 리턴한다고 가정 (given / willReturn)
when - 서비스의 특정 함수가 호출되었을 때
then - 해당하는 형식을 리턴하는지 (assertThat)
해당하는 레포지토리의 함수가 호출되었는지 (then(class).should(method)) 를 체크한다.
📌 서비스 레이어에 적합한 테스트
서비스는 외부 요인(서블렛, DB)을 배제하고 테스트할 수 있는 레이어
=> 스프링 컨테이너를 제외하고 테스트하도록 서비스 레이어에 적합한 테스트 필요
=> 유닛 테스트
📌 @ExtendWith
- 단위 테스트에 공통적으로 사용할 '확장 기능을 선언'해주는 역할
- 인자로 확장할 Extension을 지정
-> 주로 SpringExtension.class 또는 MockitoExtension.class를 사용 - @ExtendWith(SpringExtension.class) - Junit5와 Spring Test Context 프레임워크를 통합해 사용할 때 사용
- @ExtendWith(MockitoExtension.class) - JUniit5와 Mockito를 연동해 테스트를 진행할 때 사용
📌 any()
- any()는 Mockito의 ArgumentMatchers에서 제공하는 메서드
- Mock 객체의 동작을 정의하거나 검증하는 단계에서 매개변수가 중요하지 않을 때 사용
- e.g.
메서드의 실행만을 확인하고 싶거나
특정 매개변수를 설정하는게 불필요한 경우 - ❗주의❗
any( xxx.class ) 함수는 타입만 지정할 뿐 실제로 리턴하는 것은 null
즉, any( AccountUser.class ) 는 AccountUser account = null; 과 동일
// 동작 정의에 사용되는 any
given(productRepository.save(any(Product.class)))
.willReturn(returnsFirstArg());
// 검증에 사용되는 any
then(accountService).should().createAccount(anyLong(), anyLong());
📌 서비스 테스트의 시나리오
- 초기 세팅
@Mock을 사용하므로 테스트 클래스 위에 @ExtendWith(MockitoExtension.class) 작성
repository 객체를 @Mock으로 선언
service 객체를 @InsertMocks로 선언하여 자동으로 레포지토리를 주입받기 - given
레포지토리 객체의 어떤 함수가 어떤 값을 리턴하는지 설정 - when
서비스 객체의 메소드 호출 - then
서비스의 메소드가 반환한 값이 의도한 값인지를 assertThat 문으로 확인
의도한 레포지토리의 메소드가 호출되었는지를 then(repository).should(method) 메서드로 확인
📌 서비스 테스트 코드
@ExtendWith(MockitoExtension.class)
class AccountServiceTest {
@Mock
private AccountRepository accountRepository;
@Mock
private AccountUserRepository accountUserRepository;
@InjectMocks // 위에서 mock으로 만들어준 객체가 자동으로 주입됨
private AccountService accountService;
@DisplayName("최초계좌의 계좌번호는 1000000000")
@Test
void createFirstAccount() {
//given
//Account 저장에 필요한 AccountUser 객체 - 보통 따로 빼서 작성하기도 함
//❗모든 객체를 mock으로 만들 수는 없음, NPE를 막기 위한 실물 객체도 필요❗
AccountUser user = AccountUser.builder()
.name("yonso")
.id(1L)
.build();
// 아무런 계좌도 없는 상황을 설정
given(accountRepository.findFirstByOrderByIdDesc())
.willReturn(Optional.empty());
// save 함수 stub
given(accountRepository.save(any(Account.class)))
.willReturn(Account.builder()
.accountUser(user)
.build());
ArgumentCaptor<Account> captor = ArgumentCaptor.forClass(Account.class);
//when
AccountDto accountDto = accountService.createAccount(1L, 1000L);
//then
verify(accountRepository, times(1)).save(captor.capture());
Assertions.assertThat(captor.getValue().getAccountNumber())
.isEqualTo("1000000000");
}
📌 argumentCapture
- 단위 테스트는 타깃이 되는 객체만을 철저히 검증하는 테스트
-> 따라서 최대한 의존하고 있는 객체에 영향을 받지 않아야 함 - 이를 위해, 메소드의 인자로 들어가는 값을 중간에서 검증하고 싶은 경우가 생길 수 있음
- 위 코드에서는 repository.save()를 Mock으로 객체로 만들고, stub을 했으나,
그 전에 save의 인자로 들어오는 값을 검증하고 싶은 상황 - 이때 ArgumentCatptor를 활용하면 메소드에 들어가는 인자를 중간에 가로채어 테스트를 진행할 수 있음
- 선언 방법
ArgumentCaptor<T> {captor_이름} = ArgumentCaptor.forClass(T.class); - 사용 방법
verify({타겟_인스턴스}).{타겟_메서드}({정의한_ArgumentCaptor}.capture()); - 검증 방법
{정의한_ArgumentCaptor}.getValue();
참고 : [Junit5] 메소드 인자 값을 확인하고 싶을 때 - ArgumentCeptor :: Orca's Develop Blog (tistory.com)
📌 Mock 객체를 stub하지 않는다면?
- Stub하지 않은 메소드들의 경우 모키토의 기본 전략인 Answers.RETURNS_DEFAULTS에 따라
타입 별로 디폴트 값을 리턴한다. - e.g. int -> 0 / class -> null
💡 서비스 레이어 테스트 참고 (정말 잘 정리된 블로그) : [Spring] JUnit과 Mockito 기반의 Spring 단위 테스트 코드 작성법 (3/3) - MangKyu's Diary (tistory.com)