업데이트:

테스트 코드를 작성하면서 Mock 객체를 쓰게 되었고,

이를 학습하며 알게된 내용을 포스트해보도록 하겠습니다.

Test Double


먼저 테스트 더블에 대해 알아보겠습니다.

테스트 더블의 어원은 스턴트 더블에서부터 왔다고 합니다.

스턴트 더블이란, 흔히 우리가 말하는 스턴트 맨을 뜻합니다.

어원을 알고나니 테스트 더블이 뭔지 감이 오시죠?

우리가 개발하는 코드들은 Rest API 통신, Database 등 수많은 외부 환경뿐만 아니라

내가 테스트할 범위가 아닌 코드들과도 엮여있습니다.

이를 전부 실제 동작시키면서 특정 기능만 테스트하기란 분명 무리가 있겠죠.

1. 외부 의존성에 의해 테스트가 실패할 수 있다. 즉, 예상치 못한 실패를 유발할 수 있다.
2. 모든 기능을 구동시켜서 테스트를 한다면 한 가지 기능을 테스트하는데에도 시간이 오래 걸릴 수 있다.

이런 문제점을 해결하기 위해 나온 것이 Test Double입니다.

Test Double의 역할

테스트 더블의 역할은 5가지 입니다.

  1. 테스트 대상 코드를 외부요인으로 부터 격리시킨다.
  2. 테스트 속도를 개선한다.
  3. 예측 불가능한 요소를 제거한다.
  4. 정의한 상황을 시뮬레이션 한다.
  5. 감춰진 정보를 얻어낸다.

감춰진 정보를 얻어낸다.

나머지는 금방 이해가 됐는데, 개인적으로 5번이 가장 이해가 되질 않았습니다.

이해를 하고 나니 다른 말로 바꿔야하지 않나? 싶더군요.

“캡슐화된 내부 동작의 상호작용을 확인할 수 있다.”

다음은 Effective Unit Test의 예시입니다.

public class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
    }
		// ...
}
public interface Engine {
    public void start();
    public boolean isRunning();
}

CarEngine이 이런 구조일 때 Car가 출발했을 때 private으로 선언한

Enginestart를 했는지 어떻게 알 수 있을까요?

다음은 테스트 코드입니다.

public class CarTest {
		
    @Test
    void ifEngineStartWhenCarStart() {
        TestEngine engine = new TestEngine();
        new Car(engine).start();
        assertThat(engine.isRunning()).isTrue();
    }
}

public class TestEngine implements Engine {
		
    private boolean isRunning = false;
	
    public void start() {
        this.isRunning = true;
    }

    public boolean isRunning() {
        return this.isRunning;
    }
}

추상화를 이용하여 테스트를 위해 Engine을 구현한 TestEngine를 추가했습니다.

TestEngine에는 isRunning이란 변수와 메소드를 통해 start가 동작했는지를 확인하고 있습니다.

💡여기서 TestEngine은 테스트 더블 중 Fake 객체로 실제 Engine의 동작을 대신하여

start 메소드를 실행했는지를 검증하고 있습니다.

이렇게 Car 내부 Engine의 동작을 확인할 수 있는 것 역시 테스트 더블의 역할입니다.

Mock vs Stub


테스트 더블에는 크게 MockStub이 있습니다.

Stub

어떤 기능에 대해 미리 준비된 예상 답변을 주고, 그대로 응답하도록 하는 것입니다.

Mockito에선 given 메소드를 통해 해당 기능을 제공하고 있습니다.

💡원래는 Mockito에서 when 메소드를 통해 제공하고 있었습니다만

BDD(Behaviour-Driven Development)와 Given-When-Then 템플릿의 등장으로

Wrapping 한 메소드가 given입니다.

/* given 사용 예시 (memberService는 Mock객체여야 합니다.) */
given(memberService.getGoldRankMembers()).willReturn(new ArrayList<>());

💡mockito 4.0.0 버전 기준으로 기존의 when 메소드를 wrapping 하여

given 메소드를 만들 것을 확인할 수 있습니다.

public static <T> BDDMyOngoingStubbing<T> given(T methodCall) {
    return new BDDOngoingStubbingImpl<T>(Mockito.when(methodCall));
}

Mock

일반적으로 테스트를 검증하기 위해선 상태, 즉 값을 검증 하곤 합니다.

하지만 Mock행위를 검증합니다.

Mockito에선 verify 메소드를 통해 해당 기능을 제공하고 있습니다.

/* verify 사용 예시 (memberRepository는 Mock객체여야 합니다.) */
verify(memberRepository).save(new MemberEntity());

💡verify는 해당 Mock 객체가 특정 행위를 했음을 검증하는 메소드입니다.

위의 예시를 보면 memberRepositorysave 메소드가 동작했는지를 검증하고 있습니다.

이런 행위 검증은 어떨때 쓰는 것이 좋을까요?

다음 예시를 보겠습니다.

public void checkAndSaveMember(MemberEntity memberEntity) {

    if (memberEntity.isGold()) {
        memberRepository.save();
    }
}

위와 같은 예시에서 save가 되었는지, if 문의 분기가 잘 처리되는지 확인 할 때 사용할 수 있습니다.

@Test
void checkAndSaveMemberTest() {
			
    /* Given */
    MemberEntity memberEntity = new MemberEntity(Rank.GOLD)

    /* When */
    memberService.checkAndSaveMember(memberEntity);

    /* Then */
    verify(memberRepository).save(eq(memberEntity));
}

verify 부분을 통해 Given 영역에 주어진 memberEntitysave 인자로 들어가고

save가 호출되었는지를 검증하고 있습니다.

이렇게 Mock은 행위 중심의 테스트 더블이라 볼 수 있습니다.

마무리


우리가 테스트 코드, 그 중에서도 단위 테스트를 작성할 때 이제 테스트 더블은 필수가 됐지 않나 싶습니다.

오늘은 테스트 더블의 개념에 대해서 포스트를 해봤고, 다음번엔 Java에서 많이 쓰이는

Mockito에서 사용되는 어노테이션에 대해 글을 작성해보도록 하겠습니다.

오늘도 제 긴글을 읽어주셔서 감사합니다.😄

📌참고


[tdd] 상태검증과 행위검증, stub과 mock 차이

[Effective unit Testing] Chap3. 테스트 더블

댓글남기기