Mockito
Mock: 진짜 객체와 비슷하게 동작하지만 프로그래머가 직접 그 객체의 행동을 관리하는 객체.
Mockito: Mock 객체를 쉽게 만들고 관리하고 검증할 수 있는 방법을 제공한다. Mock 프레임워크 중 가장 많이 사용된다.
단순한 로직의 단위 테스트는 간단하게 할 수 있지만, 만약 애플리케이션이 DB를 사용한다던가 외부 API를 호출한다고 하면,
DAO 객체나 외부 API가 어떻게 동작하는지 Mock을 사용해 실제 API 호출 없이 사전에 테스트를 해볼 수 있다.
라이브러리 추가
스프링 부트 2.2+ 프로젝트 생성시 spring-boot-starter-test에서 자동으로 Mockito를 추가해 준다.
스프링 부트를 쓰지 않는다면, 의존성을 직접 추가한다.
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
테스트용 객체 만들기
우선 테스트에 사용할 도메인, 레포지토리, 서비스 클래스를 정의한다.
<Member>
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue
private Long id;
private String email;
}
<MemverService>
public interface MemberService {
Optional<Member> findById(Long memberId);
}
MemberService가 인터페이스로 구현되어 있고, 구현체는 따로 존재하지 않는다.
이런 경우가 Mock 객체를 사용하기 좋은 환경이다.
구현체가 없이 인터페이스만 알고 있는데, 이러한 인터페이스 기반 코드가 제대로 동작하는지 확인하기 위해 Mock 객체를 사용할 수 있다.
Mockito 테스트
우선 Mock이 없다고 가정하고 테스트를 해보자.
class StudyServiceTest {
@Test
void createStudy() {
MemberService memberService = new MemberService() {
@Override
public Optional<Member> findById(Long memberId) {
...
}
};
StudyService studyService = new StudyService(memberService);
assertNotNull(studyService);
}
}
StudyService가 MemberService 객체를 생성자 주입하는 객체라고 했을 때,
테스트 시 MemberService의 구현체가 필요하다.
그러나 따로 구현해놓은 객체가 없기 때문에 이처럼 테스트시 직접 정의해서 사용해야 한다.
그러나 일일이 메서드를 매번 정의하는 것도 번거롭다.
특히 레포지토리라도 테스트하는 경우, JpaRepository가 제공하는 모든 메서드를 구현해야 하는 아찔한 경험을 할 수 있다.
따라서 구현체를 임의로 생성할 수 있는 Mock 객체가 필요하다.
class StudyServiceTest {
@Test
void createStudy() {
MemberService memberService = Mockito.mock(MemberService.class);
StudyService studyService = new StudyService(memberService);
assertNotNull(studyService);
}
}
코드가 눈에 띄게 줄었다. Mockito를 사용해서 인터페이스의 가상의 구현체를 생성할 수 있다.
또한 간단하게 애노테이션으로도 Mock 객체를 생성할 수 있다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Mock
MemberService memberService;
@Test
void createStudy() {
StudyService studyService = new StudyService(memberService);
assertNotNull(studyService);
}
}
이 때 주의할 점은, @Mock 만 붙이면 객체가 실제로 만들어지지 않아서, @ExtendWith(MockitoExtension.class) 애노테이션을 붙여 주어야 한다.
Mock 객체 Stubbing
Stubbing은 아직 개발되지 않은 코드를 임시로 대치하는 역할을 수행한다.
아직 구현되지 않은 인터페이스를 Mock 객체로 대체했기 때문에, Mock 객체를 활용한 테스트를 위해서는 Stubbing이 필요하다.
만약 Stubbing이 없다면 기본적으로 Mock 객체의 행동은 다음과 같다.
- 기본적으로 Null(Optional의 경우 Optional.empty)을 리턴한다.
- Primitive 타입은 기본 Primitive 타입을 리턴한다. (Integer → 0)
- 컬렉션은 비어있는 컬렉션을 리턴한다.
테스트 시 findById가 Null이 아닌 원하는 값을 반환할 수 있게 Mock 객체에 Stubbing을 해보자.
우선 MemberService를 테스트해보자.
@Test
void memberServiceTest(@Mock MemberService memberService) {
Member member = new Member();
member.setId(1L);
member.setEmail("abc@email.com");
Optional<Member> findById = memberService.findById(1L);
assertEquals("abc@email.com", findById.get().getEmail());
}
memberService.findById(1L)로 객체를 조회하려 했으나 테스트는 실패한다.
앞서 알아본 것처럼 Mock 객체는 기본적으로 Null을 반환하기 때문이다. (위 예제는 Optional.empty)
따라서 아이디가 1인 객체를 조회할 수 있도록 Stubbing을 진행한다.
@Test
void memberServiceTest(@Mock MemberService memberService) {
Member member = new Member();
member.setId(1L);
member.setEmail("abc@email.com");
when(memberService.findById(1L)).thenReturn(Optional.of(member));
Optional<Member> findById = memberService.findById(1L);
assertEquals("abc@email.com", findById.get().getEmail());
}
Mockito가 제공하는 when 메서드를 사용해 리턴값을 개발자가 직접 지정해줄 수 있다.
findById가 아이디가 1인 member를 반환할 것이라 Stubbing하면 테스트는 성공하게 된다.
1L이라는 아이디 값을 직접 넣어주지 않더라도 ArgumentMathers.any()를 인자로 주면 어떠한 값이 들어가든 전부 Optional.of(member)를 반환하도록 설정할 수 있다.
when(memberService.findById(any())).thenReturn(Optional.of(member));
Stubbing의 다른 예를 보자. 이번에는 메서드에 대해 exception이 발생하도록 설정해보자.
@Test
void memberServiceTest(@Mock MemberService memberService) {
doThrow(new IllegalArgumentException()).when(memberService).validate(1L);
assertThrows(IllegalArgumentException.class, () -> {
memberService.validate(1L);
});
}
doThrow()로 when()이하의 상황에 대해 exception을 날릴 수 있다.
다음은 메서드가 동일한 매개변수로 여러번 호출될 때 각기 다르게 행동하도록 조작하는 방법이다.
@Test
void memberServiceTest(@Mock MemberService memberService) {
Member member = new Member();
member.setId(1L);
member.setEmail("abc@email.com");
when(memberService.findById(any()))
.thenReturn(Optional.of(member))
.thenThrow(new RuntimeException())
.thenReturn(Optional.empty());
Optional<Member> byId = memberService.findById(1L);
assertEquals("abc@email.com", byId.get().getEmail());
assertThrows(RuntimeException.class, () -> {
memberService.findById(2L);
});
assertEquals(Optional.empty(), memberService.findById(3L));
}
위 예제는 findById()에 대해서,
첫 번째는 Optional.of(member)를 리턴하도록,
두 번째는 RuntimeException을 던지도록,
세 번째는 Optional.empty()를 리턴하도록 설정했다.
실제로 테스트시 findById()를 호출할 때마다 순서대로 각기 다른 값을 리턴하는 것을 볼 수 있다.
특정 메서드가 몇 번 호출되었는지 확인하고 싶다면 verify()를 사용한다.
@Test
void memberServiceTest(@Mock MemberService memberService) {
StudyService studyService = new StudyService(memberService);
Member member = new Member();
member.setId(1L);
member.setEmail("abc@email.com");
when(memberService.findById(1L)).thenReturn(Optional.of(member));
studyService.createNewStudy(1L, study);
verify(memberService, times(1)).save(member);
}
테스트 도중 save() 메서드가 몇 번 수행됐는지 확인할 수 있다.
BDD 스타일 Mockito API
BDD: 애플리케이션이 어떻게 '행동' 해야 하는지에 대한 공통된 이해를 구성하는 방법으로, TDD에서 창안했다.
테스트를 작성할 때, given/when/then 구조를 사용하는 것을 한 번쯤은 들어봤을 것이다.
앞선 테스트를 다시보면 다음과 같은 구조로 나눌 수 있다.
@Test
void memberServiceTest(@Mock MemberService memberService) {
// given
StudyService studyService = new StudyService(memberService);
Member member = new Member();
member.setId(1L);
member.setEmail("abc@email.com");
when(memberService.findById(1L)).thenReturn(Optional.of(member));
// when
studyService.createNewStudy(1L, study);
// then
verify(memberService, times(1)).save(member);
}
기본적인 상황이 주어지고 (given)
어떤 특정한 상황에 대해(when)
어떤 결과가 나올지(then)
테스트하는 구조를 given/when/then 구조라고 하는데, BDD는 이러한 구조에 착안해 테스트케이스 자체에 요구사항을 만들 수 있도록 해 서 행위에 대한 테스트를 집중할 수 있도록 한다.
+) 통합테스트에 많이 사용하는 구조기이도 하다.
Mockito에서는 BddMockito라는 클래스를 통해 BDD 스타일의 API를 제공한다.
위 예제에서 BddMockito를 적용한 테스트를 보자.
@Test
void memberServiceTest(@Mock MemberService memberService) {
// given
StudyService studyService = new StudyService(memberService);
Member member = new Member();
member.setId(1L);
member.setEmail("abc@email.com");
given(memberService.findById(1L)).willReturn(Optional.of(member));
// when
studyService.createNewStudy(1L, study);
// then
then(memberService).should(times(1)).save(member);
}
given 절에서는 when이라는 표현 대신 BddMockito가 제공하는 given()을 사용해 Stubbing하였고,
then 절에서는 then()이라는 명확한 표현을 should와 함께 사용했다.
실무
Mock은 인터페이스를 미리 가상으로 구현하는 역할도 하지만,
실제 DB를 사용하지 않아도 미리 약속된 Stubbing에 따라 단위테스트를 진행함으로서, 단위테스트의 성능을 올릴 수 있다.
다음은 각 계층에 따라 실제 프로젝트에서 단위테스트를 작성한 방식이다.
서비스
@ExtendWith(MockitoExtension.class)
class AccountServiceV1ImplTest {
@InjectMocks
private AccountServiceV1Impl accountServiceV1Impl;
@Mock
private AccountRepository accountRepository;
@Test
@DisplayName("계좌번호에 대한 계좌 조회")
void findByAccountNumber() {
Account account1 = getAccount("333333000001", user1);
given(accountRepository.findByAccountNumberAndUsageStatus(account1.getAccountNumber, UsageStatus.YES))
Account account = accountServiceV1Impl.findByAccountNumber(account1.getAccountNumber, UsageStatus.YES))
assertEquals(account1.getAccountNumber(), account.getAccountNumber());
}
}
@Mock: mock 객체를 생성한다.
@InjectMocks: @Mock이 붙은 객체를 @InjectMocks가 붙은 객체에 주입시킬 수 있다.
보통 @InjectMocks(Service) @Mock(Repository) 이런식으로 Service Mock 객체에 Repository를 주입시켜 사용한다.
+) 주의할 점으로 @InjectMocks는 인스턴스를 지정해야 한다. 즉, interface같은 생성자가 없는 객체의 경우
@InjectMocks로 지정할 수 없다.
위 코드에서도 인터페이스의 구현체를 직접 Mock 객체로 지정했다.
인터페이스를 Mock 객체처럼 사용하고 싶다면, 코드를 아래처럼 바꾸면 된다.
@ExtendWith(MockitoExtension.class)
class AccountServiceV1ImplTest {
private AccountServiceV1 accountServiceV1;
@Mock
private AccountRepository accountRepository;
@BeforeEach
public void setUp() {
MockitoAnnotations.initMocks(this);
accountServiceV1 = new AccountServiceV1Impl(accountRepository);
}
...
}
컨트롤러
@WebMvcTest(AccountControllerV1.class)
@MockBean(JpaMetamodelMappingContext.class)
public class AccountControllerV1Test {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private AccountServiceV1 accountServiceV1;
private User user1;
private Account account1;
private final String baseURI = "/v1/api/account";
@BeforeEach
void initialize() {
user1 = getUser(1L, "han1");
account1 = getAccount("333333000001", user1);
}
@Test
@DisplayName("계좌 잔액 조회")
void getAccountsBalance() throws Exception {
AccountRequestDto request = new AccountRequestDto(account1.getAccountNumber)
AccountResponseDto response = AccountResponseDto.from(account1);
given(accountServiceV1.findByUserAndAccountNumber(user1.getId(), request)).w
mvc.perform(post(baseURI + "/balance")
.header("Authorization", userId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status", equalTo("OK")))
.andExpect(jsonPath("$.data.accountNumber", equalTo(response.getAccountNumber)))
}
}
클래스
@WebMvcTest: ApplicationContext(스프링 컨테이너)를 완전하게 동작시키지 않고, 컨트롤러 테스트를 하고 싶을 때 사용한다.
Present Layer 관련 컴포넌트만 스캔한다.
필드
MockMvc: 애플리케이션을 배포하지 않고도, 서버의 MVC 동작을 테스트할 수 있는 라이브러리이다. 주로 컨트롤러 단위테스트에 많이 사 용한다.
ObjectMapper: MockMvc에 request body를 넣는 경우 문자열로 넣어야 하는데, 이 때 json → string을 매핑시켜주는 역할을 한다. 주 의할 점으로 request DTO에 기본 생성자가 있어야 한다. (기본 생성자 + setter를 통해 serialize)
@MockBean: 컨트롤러에서 서비스 레포지토리 등의 dependency가 필요한 경우에 @MockBean을 사용한다. @Mock과 달리 스프링 컨테이너에 등록할 필요가 있는 경우에 사용한다.
+) 클래스 위에 @MockBean(JpaMetamodelMappingContext.class)을 붙이는 이유
Jpa Auditing을 사용하는 경우, Application 클래스에 @EnableJpaAuditing을 삽입하는 경우가 있다.
컨트롤러 테스트는 이 Application 클래스가 항상 로드되면서 실행이 된다.
여기에 Auditing 애노테이션이 등록되어 있어서 모든 컨트롤러 테스트는 Jpa 관련 빈을 필요로 하는 상태이지만,
@WebMvcTest는 JPA에 관련된 빈은 등록하지 않기 때문에, JPA 관련 MockBean을 클래스 범위에 지정하는 것이다.
참고자료
참고자료 https://scshim.tistory.com/439
https://cornswrold.tistory.com/369
https://twer.tistory.com/entry/Mock과-Mocktio-Mock-MockBean
https://beaniejoy.tistory.com/76
https://velog.io/@lsj8367/JPA-metamodel-must-not-be-empty
'java > java' 카테고리의 다른 글
[Java] Comparator와 @FunctionalInterface (0) | 2024.03.03 |
---|---|
[Java] public class (0) | 2024.02.23 |
[Java] 등가속도 운동 - t초 후의 위치 계산 (0) | 2023.01.11 |
[Java] 메서드 애노테이션 정보 가져오기 (3) | 2022.03.12 |
[Java] 현재 실행 중인 메서드 이름 가져오기 (2) | 2022.03.12 |