본문 바로가기
가치관 쌓기/개발 돌아보기

Mockito을 왜 쓰는지 모르겠다? (with. Test Double)

by simplify-len 2022. 9. 22.

안녕하세요. 렌입니다.

오늘은 우리가 테스트 코드 작성할 때마다 Mockito 을 사용하는데, 왜 사용하는지 대해서 조금 더 깊게 이야기해볼까 합니다.

테스트하고자 하는 객체의 의존성을 격리한다. 라는 것이 우리가 흔히 아는 왜 사용하는지에 대한 답인데요.

여기서 Mocktio 는 Test Double 라는 큰 개념 안에 있는 하나입니다.

Test Double에 대해서 알게 된다면 Mocktio 을 왜 사용하는지 조금은 이해할 수 있지 않을까요?

When we are writing a test in which we cannot (or chose not to) use a real depended-on component (DOC), we can replace it with a Test Double.
The Test Double doesn’t have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the SUT thinks it is the real one!

테스트 코드를 작성할 때 실제 DOC(depoended-on component; 의존 구성 요소)를 사용할 수 없다면, DOC 대신 테스트 더블로 대체할 수 있음.
테스트 더블은 실제 DOC와 똑같이 행동하지 않아도 되며, 똑같은 API만 제공하면 된다.
Gerard Meszaros

Test Double 의 종류

어떤 Test Double이 있을까요? 흔히 4가지를 말합니다.

  1. Stub
  2. Fake
  3. Spy
  4. Mock

Stub

Stub 이라는 말을 어디서 들어봤을까요? Stub 의 뜻은 끝이 잘렸거나 유난히 짫은 것 이라는 의미를 가집니다.

Stub 의 목적은 원래의 구현을 최대한 단순한 것으로 대체하는 것입니다. 예를 들면, 어떤 객체의 기본값을 반환하는 등의 행위를 의미할 수 있습니다.

Stub 이란 테스트 더블은 최대한 단순하게 만들어 테스트 격리를 유도하거나 최소한의 행위만을 보장하는 객체을 의미하는 것이라 생각했습니다.

뒤에서 설명할 Fake, Mock 과 비슷하다고 생각할 수 있지만, 적어도 제가 생각하기에는 테스트가 가진 기능이 가장 없는 것이 바로 Stub이 아닐까 생각합니다.

만약 Stub 이 최소한의 기능을 가졌다면, 그것보다 좀 더 많은 기능(ex, 입력에 따라 다른 결과를 반환하는 것)이 필요하다면 Fake 을 고려할 수 있습니다.

Fake

Fake 는 앞서 설명한 것 처럼 Stub 보다는 조금 더 많은 기능을 가졌습니다.

Fake 객체는 진짜 객체의 행동을 흉내내면서, 진짜 객체를 사용할 때 생기는 부수 효과나 연쇄동작이 일어나지 않도록 경량화하고 최적화한 것입니다.

interface UserRepository {
    void save(User user);
    User findById(long id);
    User findByUsername(String username);
}

위와 같은 인터페이스가 있다면, Stub 을 만들 수도 있지만 간단한 인메모리 데이터베이스를 만들어 사용할 수 있습니다.

class FakeUserRepository implements UserRepository {
    private Collection<User> users = new ArrayList<User>();

    public void save(User user){
    ...
    }
    ... 중략
}

위와같이 사용하는데, 실제 객체를 사용하는 것보다 더 빠르게 동작할 뿐만 아니라, 테스트에서 DB의 의존성 또한 격리시키게 되었습니다.

Spy

테스트 스파이는 기밀을 훔친다. - p71 이펙티브유닛테스팅

왜 Spy 는 기밀을 훔칠까? Spy 는 Stub 이나, Fake 처럼 개발자가 임의로 만든 객체가 아닌 실제 객체를 사용합니다.

Spy 을 사용하는 이유는 단순합니다.

테스트 코드를 작성하다 보면 종종 void 을 반환하는 메서드를 어떻게 테스트해야 되지? 라는 생각을 하게 됩니다.

public <T> void  filter(List<T> list, Predicate<T> predicate) {
        list = list.stream().filter(predicate).collect(Collectors.toList());
}
// filter 가 잘 동작됨을 어떻게 확인할 수 있지?

이럴 때 사용할 수 있는 Spy 입니다. 마치 잠복근무를 하는 경찰이 목격한 내용을 보고하는 것과 비슷합니다.

@Test
    void name2() {

        List<String> strings = new SpyList<>();
        strings.add("A");
        strings.add("B");

        removefilter(strings, (t) -> t.equals("A"));
        assertThat(strings.has("A")).isFalse();
        assertThat(strings.has("B")).isTrue();
    }

Mocktio 에서는 @Spy 에 대한 구현이 있습니다.

테스트 스파이는 목격한 일을 기록해두었다가 나중에 테스트가 확인할 수 있게끔 만들어진 테스트더블입니다.

Mock

Spy 가 실제 객체를 활용해 기록했다가 테스트가 확인하는 용도라면, Mock 은 어떤 것이든 테스트가 원하는 것을 반환하도록 해주는 테스트 더블입니다.

특정 조건이 발생하면 미리 약속된 행동을 취합니다.

그렇다면 Test Double이 전달해주는 가치에 대해서 이야기해봅시다.

  1. 테스트하고자 하는 객체의 의존성을 격리한다.
  2. 테스트의 속도를 더 빠르게!
  3. 예측 불가능한 실행 요소를 제거한다.

테스트하고자 하는 객체의 의존성을 격리한다.

격리를 한다는 것은 코드을 2가지로 분류할 수 있다는 이야기입니다.

  • 테스트 대상 코드
  • 테스트 대상 코드와 상호작용하는 코드

img

테스트 대상 코드를 상호작용하는 코드로부터 의존성을 분리하는 것을 의미합니다.

분리함으로써,

테스트는 초점이 분명해지고, 이해하기도 쉬워지며, 테스트 코드에 필요한 설정이 간편해집니다.

테스트의 속도를 더 빠르게!

위에서 언급한, Stub, Fake, Spy, Mock 등의 테스트 더블을 활용하게 된다면,

전에 계산해둔 경로를 반환하도록 하여 쓸데없이 기다리는 시간이 줄어들어 테스트 속도를 더 빠르게 개선할 수 있게 됩니다.

예측 불가능한 실행 요소를 제거한다.

테스트 더블을 사용해야되는 이유는 무엇을 테스트하고 싶은 것인지 명확히 할 수 있다.

예를 들어, 현재 시간에 의존적인 테스트가 있다면 어떻게 테스트 할 것인가? 그런 부분을 테스트 더블을 활용해 격리하여

테스트하고자 하는 곳에 집중할 수 있게 됩니다.

[연관 포스팅]

좋은-테스트에-대해서-한번-떠들어보자in-이펙티브유닛테스팅

[참고자료]

Effective Unit Testing

댓글