Mock 이란?
Mock은 진짜 객체와 비슷하게 동작하지만 프로그래머가 직접 그 객체의 행동을 관리하는 객체이다.
개발이 덜된 API를 이용할 경우나, 테스트 진행에 외부 API가 필요한 경우 등
외부API를 신경 안 쓰고 객체를 테스트할 때 사용한다.
Mocking
Mocking은 unit 테스트에서 주로 등장하는데, 테스트 대상의 객체에 의존되어 있는 다른 객체들을 페이크 객체로 만드는 것이다
ex)
테스트 대상은 다른 객체, 함수 간의 의존성을 가지고 있을 수 있다.
회원 가입을 하기 위해서는 ID, PW를 저장하면 되지만,
실제로 ID 중복체크도 있고, 비밀번호 유효성 검사, 암호화 등등 하나의 행동에는 실제로 다양한 의존 관계가 존재한다.
하지만, 나는 ID / PW 가 제대로 저장되는지만 알고 싶고 이를 테스트 하고 싶다.
이를 위해서는 ID 중복체크는 무조건 True, 비밀번호 유효성 검사도 무조건 True, 암호화는 ‘a123’으로 만들어 준다고 가정해본다.
DB insert 되기 전 객체의 데이터는 { ID: 'idid', PW: 'a123' }
라면, 정상적으로 회원 가입 로직을 수행한 것으로 볼 수 있을 것이다.
위에서 회원 가입이라는 행동에 의존된 것들을 가짜 행위한다고 가정한 것이 Mocking이다.
테스트 대상이 아닌 것들을 Fake Object, Fake Action 처리하는 것을 의존성을 Mocking 처리했다고 말할 수 있다.
이처럼 Mocking은 테스트 대상 객체의 행동을 고립시키기 위해서,
의존성을 배제하기 위해서는 의존되어있는 객체들의 행동을 가짜로 만들어주는 것이다.
Mockito
Mockito 란 단위 테스트를 위한 Java mocking framework이다.
스프링부트 2.2 이상이라면 spring-boot-starter-test에 Mockito가 포함되어있다.
*Mockito 없이 직접 Mock을 만들 수 있는데 blog에 있는 글을 보면 된다.
단점은 의존 개수와 크기에 따라 구현하는데 많은 부담감이 생긴다고 한다.
1. Mockito를 이용하여 Mock 생성하기
1
2
3
4
5
6
7
8
9
10
11
12
class CommentServiceTest {
@Test
void mockito_test() {
UserRepository userRepository = mock(UserRepository.class);
RegistryRepository registryRepository = mock(RegistryRepository.class);
CommentRepository commentRepository = mock(CommentRepository.class);
CommentServiceImpl commentService = new CommentServiceImpl(commentRepository, registryRepository, userRepository);
assertThat(commentService).isNotNull();
}
}
2. @Mock 어노테이션으로 생성하기
먼저 class에 어노테이션을 붙여준다. JUnit5 extension을 이용하여 사용할 수 있다.
1
2
3
@ExtendWith(MockitoExtension.class)
class RegistryServiceTest {
}
@ExtendWith(MockitoExtension.class)
: test 클래스가 Mockito를 사용함을 의미한다.
@Mock 사용방법은 2가지로 나눠진다.
- 생성자 주입 2.
@Mock
객체 주입
방법 1. 주입된 @Mock 객체로 직접 생성자에 주입
1
2
3
4
5
6
7
8
9
10
@ExtendWith(MockitoExtension.class)
class RegistryServiceTest {
@Test
void mockito_test(@Mock UserRepository userRepository,
@Mock RegistryRepository registryRepository){
RegistryServiceImpl registryService = new RegistryServiceImpl(registryRepository, userRepository);
assertThat(registryService).isNotNull();
}
}
@Mock
: 실제 구현된 객체 대신에 Mock 객체를 사용하게 될 클래스를 의미한다.
테스트 런타임 시 해당 객체 대신 Mock 객체가 주입되어 Unit Test가 처리된다.
방법 2. @InjectMocks를 통해 자동으로 @Mock 객체를 주입
1
2
3
4
5
6
7
8
9
10
11
12
13
@ExtendWith(MockitoExtension.class)
class RegistryServiceTest {
@Mock
UserRepository userRepository;
@InjectMocks
RegistryServiceImpl registryService;
@Test
void mockito_test(){
assertThat(registryService).isNotNull();
}
}
@InjectMocks
: Mock 객체가 주입된 클래스를 사용하게 될 클래스를 의미한다.
테스트 런타임 시 클래스 내부에 선언된 멤버 변수들 중에서
@Mock
으로 등록된 클래스의 변수에 실제 객체 대신 Mock 객체가 주입되어 Unit Test가 처리된다.
이를 토대로 정리해보면
RegistryServiceTest는 Mockito를 사용(@ExtendWith
)하고, UserRepository를 실제 객체가 아닌 Mock 객체로 바꾸어 주입(@Mock
)한다.
따라서 테스트 런타임 시 RegistryServiceImpl의 멤버 변수로 선언된 UserRepository에 Mock 객체가 주입(InjectMocks)된다.
@MockBean
@MockBean
은 Mock과 달리 org.springframework.boot.test.mock.mockito
패키지 하위에 존재한다.
즉 spring-boot-test에서 제공하는 어노테이션이다. Mockito의 Mock 객체들을 Spring의 ApplicationContext에 넣어준다.
그리고 동일한 타입의 Bean이 존재할 경우 MockBean으로 교체해준다.
@MockBean
은 스프링 컨텍스트에 mock객체를 등록하게 되고
스프링 컨텍스트에 의해 @Autowired
가 동작할 때 등록된 mock 객체를 사용할 수 있도록 동작한다.
언제 @Mock
을 쓰고 언제 @MockBean
을 쓸까?
Spring Boot Container가 필요하고 Bean이 container에 존재 해야한다면 @MockBean
을 쓰고 아니라면 @Mock
을 쓰면 된다.
@Mock
은 @InjectMocks
에 대해서만 해당 클래스안에서 정의된 객체를 찾아서 의존성을 해결한다.
@MockBean
은 mock 객체를 스프링 컨텍스트에 등록하는 것이기 때문에 @SpringBootTest
를 통해서 Autowired에 의존성이 주입되게 된다.
@Autowired
라는 강력한 어노테이션으로 컨텍스트에서 알아서 생성된 객체를 주입받아 테스트를 진행할 수 있도록 한다.
Mock 종류 | 의존성 주입 |
---|---|
@Mock | @InjectMocks |
@MockBean | @Autowired |
🐣 @Mock
을 사용하면 의존성 주입으로 @InjectMocks
을 쓰고 @MockBean
을 사용하면 주입할 때 @Autowired
를 쓰면 된다.
spring에서는 @Mock
을 쓰나 @MockBean
을 쓰나 상관없다.
다만 스프링 부트를 쓰지 않고 자바만을 가지고 사용한다면 @mock
으로 작성해야한다.
Mockito로 test 코드 작성하기
Mockito는 Stub 작성과 Verify가 중심을 이루며 다음과 같은 순서로 진행된다.
CreateMock : 인터페이스에 해당하는 Mock 객체를 만든다.
Stub : 테스트에 필요한 Mock 객체의 동작을 지정한다.(필요시만)
Exercise : 테스트 메소드 내에서 Mock객체를 사용한다.
Verify : 메소드가 예상대로 호출됐는지 검증한다.
스터빙(Stubbing)이란?
Stub은 테스트가 실행되고 통과할 정도로만 구현된다.
만들어진 mock 객체의 메소드를 실행했을 때 어떤 리턴 값을 리턴할지를 정의하는 것이다.
Mock과 Stub의 차이점
Stub의 대표적인 예제는 Controller - Service 관계를 들 수 있다.
Controller는 일반적으로 Service에서 가공된 데이터를 가져온다.
하지만, Controller의 응답을 테스트함에 있어서 Service가 어떻게 실행되는지 궁금하지 않다.
Service로부터 반환받은 데이터를 클라이언트에게 제대로 응답하는지만 궁금할뿐이다.
이 경우, Service라는 것을 정의할 필요가 없다. 클라이언트로 전해줄 정적 데이터만 필요할 것이다.
이런 경우 “Stubbing한다”라고 표현할 수 있다.
- Stub - 단순한 테스트용이라, 대체한 객체에 대한 정확한 검증하지 않음
- ex) 인메모리로 가상 데이터
- Mock - 테스트 대상이 의존하는 객체를 사용하고 동일한 역할을 흉내내고 있는지까지 검증
- 이 데이터에 실제 사용하려는 DB에 정확하게 동작하는가
Mockito에선 when 메소드를 이용해서 스터빙을 지원하고 있다.
스터빙을 할 수 있는 방법은 OngoinStubbing, Stubber를 쓰는 방법 2가지가 있다.
OngoingStubbing 메소드
OngoingStubbing 메소드란 when에 넣은 메소드의 리턴 값을 정의해주는 메소드이다.
when({스터빙할 메소드}).{OngoingStubbing 메소드};
메소드명 | 설명 |
---|---|
thenReturn | 스터빙한 메소드 호출 후 어떤 객체를 리턴할 건지 정의 |
thenThrow | 스터빙한 메소드 호출 후 어떤 Exception을 Throw할 건지 정의 |
thenAnswer | 스터빙한 메소드 호출 후 어떤 작업을 할지 custom하게 정의, mockito javadoc을 보면 이 메소드를 굳이 사용하지 말고 thenReturn, thenThrow 메소드 사용을 추천하고 있다. |
thenCallRealMethod | 실제 메소드 호출 |
Stubber 메소드
{Stubber 메소드}.when({스터빙할 클래스}).{스터빙할 메소드}
메소드명 | 설명 |
---|---|
doReturn | 스터빙 메소드 호출 후 어떤 행동을 할 건지 정의 |
doThrow | 스터빙 메소드 호출 후 어떤 Exception을 throw할 건지 정의 |
doAnswer | 스터빙 메소드 호출 후 작업을 할지 custom하게 정의 |
doNothing | 스터빙 메소드 호출 후 어떤 행동도 하지 않게 정의 |
doCallRealMethod | 실제 메소드 호출 |
테스트에 사용할 스텁 만들기
when(Mock_객체의_메소드).thenReturn(리턴값);
when(Mock_객체의_메소드).thenThrow(예외);
스텁은 필요할 때만 만드는 것이 원칙이다. 메소드 호출 여부를 검증만 할 때는 사용하지 않는다.
아래처럼 내부에서 인스턴스를 생성해서 사용하기 때문에 매개변수를 제어 못하는 경우가 있다.
1
2
3
RegistryDto registry1 = new RegistryDto();
registry1.setTitle("첫 번째");
registry1.setMain("1");
제어할 수 없는 매개변수는 any()를 이용하여 처리한다.
1
when(registryService.postUpload(any(), user)).thenReturn(saveRegistry);
검증
스터빙한 메소드를 검증하는 방법
verify 메소드를 이용해서 스터빙한 메소드가 실행됐는지, n번 실행됐는지, 실행이 초과되지 않았는지 등 다양하게 검증해볼 수 있다.
verify(T mock, VerificationMode mode)
VerificationMode는 검증할 값을 정의하는 메소드이다.
메소드명 | 설명 (테스트 내에서~) |
---|---|
times(n) | 몇 번이 호출됐는지 검증 |
never | 한 번도 호출되지 않았는지 검증 |
atLeastOne | 최소 한 번은 호출됐는지 검증 |
atLeast(n) | 최소 n 번이 호출됐는지 검증 |
atMostOnce | 최대 한 번이 호출됐는지 검증 |
atMost(n) | 최대 n 번이 호출됐는지 검증 |
calls(n) | n번이 호출됐는지 검증 (InOrder랑 같이 사용해야 함) |
only | 해당 검증 메소드만 실행됐는지 검증 |
timeout(long mills) | n ms 이상 걸리면 Fail 그리고 바로 검증 종료 |
after(long mills) | n ms 이상 걸리는지 확인timeout과 다르게 시간이 지나도 바로 검증 종료가 되지 않는다. |
description | 실패한 경우 나올 문구 |
실습
Service test 코드에는 Service가 주된 test가 되어야 한다.
그 말은 repository는 mock을 통해 가짜 객체를 만들어 사용하는 것이다.
이 전에는 @Autowired
를 이용해서 Service와 Repository를 사용했었다.
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
33
34
35
36
37
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Transactional
class RegistryServiceTest {
@Autowired RegistryRepository registryRepository;
@Autowired UserRepository userRepository;
@Autowired RegistryServiceImpl registryService;
@Test
void register() throws Exception {
//given
User user = User.builder()
.username("username")
.nickname("nickname")
.password("password")
.email("email")
.build();
User saveUser = userRepository.save(user);
UserDetailsImpl userDetails = new UserDetailsImpl(saveUser);
RegistryDto registry1 = new RegistryDto();
registry1.setTitle("첫 번째");
registry1.setMain("1");
RegistryDto registry2 = new RegistryDto();
registry2.setTitle("두 번째");
registry2.setMain("2");
//when
Registry saveRegistry1 = registryService.postUpload(registry1, userDetails);
Registry saveRegistry2 = registryService.postUpload(registry2, userDetails);
//then
Assertions.assertThat(registry1.getTitle()).isEqualTo(saveRegistry1.getTitle());
Assertions.assertThat(registry2.getTitle()).isEqualTo(saveRegistry2.getTitle());
}
}
Mockito 적용하기
- @Mock
- mock 객체를 만들어서 반환한다.
- @InjectMocks
- @Mock이나 @Spy 객체를 자신의 멤버 클래스와 일치하면 주입시킨다.
- @Spy
- 실제 객체를 생성하고 필요한 부분에만 mock처리하여 검증을 진행할 수 있다.
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
33
34
35
36
37
38
39
40
41
42
43
44
@ExtendWith(MockitoExtension.class) // 1번
class RegistryServiceTest {
@Mock
UserRepository userRepository;
@Mock
RegistryRepository registryRepository;
@InjectMocks
RegistryServiceImpl registryService;
@Test
void mockito_test(){
assertThat(registryService).isNotNull();
}
@Test
void register() throws Exception {
//given
List<String> role = Collections.singletonList("ROLE_USER");
User user = User.builder()
.name("username")
.nickname("nickname")
.password("password")
//.email("email")
.roles(role)
.build();
//userRepository.save(user); // 2번
RegistryDto registryDto = new RegistryDto();
registryDto.setTitle("첫 번째");
registryDto.setMain("1");
Registry saveRegistry = registryDto.toEntity(user);
when(registryRepository.save(any(Registry.class))).thenReturn(saveRegistry); // 3번
//when
Registry registry = registryService.postUpload(registryDto, user);
verify(registryRepository).save(any(Registry.class)); // 4번
//then
Assertions.assertThat(registryDto.getTitle()).isEqualTo(registry.getTitle()); // 5번
Assertions.assertThat(registryDto.getMain()).isEqualTo(registry.getMain());
}
}
1번
@Mock
을 사용하기 위해서는 @ExtendWith(MockitoExtension.class)
를 사용한다.
통합 테스트로 진행하는 것이 아니므로 @SpringBootTest와 @Transactional을 삭제했다.
2번
userRepository.save(user);
를 주석처리한 이유는 RegistryService를 test 하는 코드를 작성하는 것이기 때문에
userRepository.save(user)는 성공했다고 가정하고 진행하므로 필요없는 코드이다.
3번
when(registryRepository.save(any(Registry.class))).thenReturn(saveRegistry);
test의 registryRepository와 실제 ServiceImpl의 registryRepository가 같아야 하므로 any()를 넣어준다.
any안에 class를 안넣어줘도 되긴하지만 넣어줬다.
4번
verify(registryRepository).save(any(Registry.class));
Stubbing을 해줬으니 검증을 해본다.
5번
Assertions.assertThat(registryDto.getTitle()).isEqualTo(registry.getTitle());
service를 test하는 것이므로 saveRepository를 기댓값으로 넣지 않게 주의한다.
Mockito 알아보기 (부제 : BDD)
[Spring boot TEST] Mockito input을 그대로 리턴하기
[SpringBoot]@Mock,@MockBean 차이가 뭘까?
[JUnit/Spring] Mockito annotion 차이(@Mock , @MockBean , @Spy , @SpyBean)
모키토 프레임워크(Mockito framework)
[Java] Mockito 사용법 (3) - 스터빙 (Stubbing) (OngoingStubbing, Stubber)
[Java] Mockito 사용법 (4) - 검증 (Verify)
Unit Test에 나오는 Fixture와 Mock은 무엇일까?
[TDD] Fixture와 Mock이란?
Mockito란?