Home Mock
Post
Cancel

Mock

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가지로 나눠진다.

  1. 생성자 주입 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란?

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