Development Logs/Spring Ecosystem

[Spring Test] Test Double에 대한 소개

유뱅유뱅뱅 2023. 7. 17. 23:17

0. 들어가면서


Kent BeckXP(Extreme Programming) Explained 저서에는 이런 문구가 있습니다.

개발의 기본 흐름은 일단 실패하는 테스트를 작성하고, 그 다음으로 그 테스트를 통과하도록 만드는 것이다.

해결하고 싶은 스토리들을 목록으로 만들고, 그 스토리들을 표현하는 테스트들을 작성하고, 그런 다음 그 테스트들 통과하도록 만든다. 여러분이 작성해야 할 필요가 있다고 생각하는 테스트들을 목록으로 만들고, 테스트를 하나 작성하고, 그 테스트를 통과하도록 만들고, 다른 테스트를 작성하고, 두 테스트 모두 통과하도록 만들고 하면서 목록이 비워질 때까지 일한다.

이번에 VmWare Tanzu Labs분들에게 TDD로 진행하는 법을 배우면서, 테스트 주도 개발(TDD, Test Driven Development)는 실천을 통해서 얻을 수 있는 느낌을 많이 받았다. TDD를 어느 상황에 어떻게 한다 라고 정의 내리긴 힘들고 각 상황별, 자기가 테스트하고 싶은 대상에 따라, TDD를 진행하는 방식이 달라질수 있다는 느낌을 많이 받았다.

그렇지만, 개발자는 TDD를 실천하기 위한 몇가지 도구들이 필요하고, 그 중 Test Double의 개념 설명과 그 것을 구현하는 도구인 Mockito에서 지원하는 Mock, Spy에 대해 간략하게 소개하고자 한다.

 

 

1. Test Double


테스트 더블(Test Double)이라는 단어는 영화 산업에서 위험한 장면을 촬영할 때 배우를 대체할 대역인 스턴트 더블(stunt double)에서 유래했습니다. 테스트를 진행할 때 실제 클래스를 사용하는 것이 아니라 이와 동일한 형태를 가진 테스트 더블을 사용합니다.

 

1.1. Why Using Test Double

단위 테스트(unit test)는 시스템이 커질수록 쉽지 않아지기 마련입니다. 테스트하고 싶은 메소드 내부에 다른 컴포넌트(component)에 의존한 기능들로 인해 결합도(coupling)가 높을수도 있고, 제어하기 어려운 네트워크나 데이터베이스를 사용하는 기능들이 존재할 수 있습니다. 이런 여러 가지 제약 사항들 때문에 어려운 테스트를 빠르고 쉽게 진행하기 위해 테스트 더블을 사용합니다.

시스템 컴포넌트 단위 테스트

https://www.crocus.co.kr/1555

 

1.2. When Using Test Double

테스트 더블은 다음과 같은 시기에 사용합니다.

  • 예측 불가능한 요소를 통제하여 테스트하기를 원하는 경우
  • 느린 테스트를 보다 빠르게 진행하기를 원하는 경우
  • 통합 환경 구축의 어려움이 발생하는 경우
  • 실제 클래스를 사용하기 어렵고 불편한 경우

 

 

2. Test Double 소개


  • 테스트를 도와주는 개념적인 요소들이다.
  • Test Double에는 5가지 Type이 있다.

TestDouble의 5가지 Type

 

2.1. Test Double Type별 소개

  • 여기서 sut(System Under Test)가 의미하는 것은 테스트하고자 하는 대상을 말한다.
  • 여기서 예제 코드들은 Mockito의 Mock을 사용하여 대부분 작성하였다.

Mock

Mock

  • sut의 메소드가 실행될때, 일어나는 sut의 어떤 행위를 검증하고자 할때, 사용한다.
    • 예를들면, sut는 BarController이고, BarController 메소드를 실행하면, BarAdaptor.getLineCount(request)이란 행위를 한다고 하자
    • 이때, 아래와 같이 검증할수 있고, 이것을 Mock이라고 한다.
verify(BarAdaptor).getLineCount(request);

 

Spy

Spy

  • 직접 customizing한 mock이라고 할 수 있다.
    • 테스트에 사용되는 객체, 메소드의 사용 여부 및 정상 호출여부를 주로 알려주기 위해 사용되며, 테스트에 사용되는 객체를 implements 또는 extends 받아서, @Override 해서 직접 메소드를 customizing 해서 테스트할 수 있다.
  • customizing
public class BarClientSpy implements BarClient {
    private final List<Integer> callSequences = new ArrayList<>();

    public List<Integer> getCallSequences() {
        return this.callSequences;
    }

    @Override
    public BarResDTO bar(BarReqDTO request) {
        callSequences.add(1);
        return stubResponse();
    }
    
    private BarResDTO stubResponse() {
        return BarResDTO.builder
                .cd("sampleCd")
                .msg("sampleMsg")
                .build();
    }
}

 

Stub

Stub

  • sut의 메소드가 실행될 때, 일어나는 어떤 행위를 정의(준비해둔 결과를 반환)해줄 때 사용한다.
    • 어떤걸 리턴해줘라(행위)
      • .thenAnswer(추가 제어; return 반환값)
        • 어떤 행위(when)을 했을때, 필연적으로 제어하는 로직이 필요한 경우, thenAnswer를 이용할 수 있다.
when(barAdaptor.getLineCount(request))
                .thenAnswer((invocation) -> {
                    ResponseHeadersContext.setResponseHeader(
                            Map.of("transaction-id", List.of("12345"))
                    );
                    return BarResDTO.builder()
                                    .cd("sampleCd")
                                    .msg("sampleMsg")
                                    .build()
                                );
                });
    • .thenReturn(반환값)
when(barClient.bar(a, b))
                .thenReturn(
                        BarResDTO.builder()
                        		.cd("sampleCd")
                                .msg("sampleMsg")
                                .build()
                );

 

Fake

Fake

  • 테스트를 위해 실제처럼 동작해주는 객체들이다.
  • 주로 외부시스템인 경우 많이 사용하며, 비즈니스 로직들을 추가해서 만들어도 fake가 될 수 있다.
    • ex) wiremock, embededRedis
    • WireMock.stubFor()
stubFor(
        WireMock.post("/bar")
                .withRequestBody(
                        containing(encodeUrl)
                )
                .willReturn(
                        aResponse()
                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
                                .withBody(response)
                )
);

 

Dummy

Dummy

  • 특정 검증하고자 하는 메소드에 어떤값이 들어가도 상관 없는 값이다.
    • 들어가지 않으면 컴파일러가 되지 않는 경우 dummy를 넣어준다.
    • any()
sut.call(a, any())

 

 

3. Mockito 소개


  • 테스트를 도와주는 도구의 종류이다.
  • Mockito에서 지원하는 테스트 도구에는 대표적으로 2가지가 있다.

Mockito에서 지원하는 테스트 도구

 

Mock

  • 여기서 지원하는 mock이란 위에서 말하는 개념적인 mock과는 다른 테스트를 도와주는 도구라고 생각해야한다.
  • Mockito.mock : 테스트 더블 (아직 어떻게 쓸지 모르기 때문에 테스트 더블 상태이다)
    • 그리고 어떻게 사용할지에 대해 아래와 같이 구분되게 되는것이다.
    • Mockito.mock().thenReturn → Stub
    • verify() -> Mock
  • @MockBean
    • @SpringBootTest, @WebMvcTest 등에서 사용한다.(통합테스트에서 쓰는것임)
    • mock() 이랑 역할이 똑같다.
    • Spring이 bean 으로 등록된 애들을 사용하므로, 의존성 관계를 생각해서 mockbean으로 등록해줘야하는 경우가 생길 수 있다.

 

Spy

  • Spy는 when then 안해주면 null이된다.
  • when then 안해주면 구현되어있는데로 동작한다.

 

Q&A


  • @SpringBootTest 또는 그냥 unit test 를 하는 기준?
    • spring의 힘이 필요한 경우..ㅎㅎ
    • 가능한 unit test로 진행하는게 좋다
    • 보통 외부랑 접점이 있는 경우, 많이 사용하는 것 같기도 함
    • 최대한 의존성을 간단히 가져가야된다.(추상화 Layer를 둘 수 있도록 한다)
  • 테스트 코드를 짜다보면 의존성을 주입을 많이 가지게 되는 경우가 생기게 된다.
    • 이때, 최대한 의존성을 간단히 가져갈 수 있도록 해야하며, 추상화 Layer를 두어 해결할 수 있다.
    • ex) authservice - kakao 예시
기존 추상화 Layer를 두어서 역할 구분

 

 

Reference