져니의 개발 정원 가꾸기

Test Double (테스트 대역) 본문

개발노트/TDD

Test Double (테스트 대역)

전전쪄니 2023. 7. 30. 22:56

*이 글은 마틴 파울러(Martin Fowler)와 Gerad Meszaros의 글을 참고하여 작성했습니다.

연습중인 스턴트 배우

애플리케이션에서 테스트할 대상(SUT - system under test)이 테스트 환경에서 쓸 수 없는 운영 코드의 클래스 혹은 컴포넌트에 의존하고 있을 때 테스트 코드 작성의 어려움을 겪곤 한다. 실제 운영 코드의 클래스를 테스트 환경에서 그대로 사용할 경우, 테스트에 필요한 입출력을 통제할 수 없거나, 테스트 코드의 실행이 적절치 않은 부작용(side-effect)를 만들어낼 수 있기 때문이다.

따라서 테스트하고자 하는 로직만 고립시켜 검증하기 위해서는, 실제 운영코드를 대신하는 테스트 객체 혹은 컴포넌트가 필요하다. 이처럼 대신하는 코드를 테스트 대역이라고 한다. 테스트 대역이 무엇이고, 어떠한 것들이 있는지 알아두면 테스트 코드를 작성하는데 많은 도움을 받을 수 있다.

Test Double이란?

정의 : 테스트 환경에서 실제 운영코드를 대신하는 대역 (= 테스트 대역)

Double이란, 영어로 대역배우 (소위 스턴트 배우 라고도 많이 얘기한다.)를 의미한다. 즉, Test Double은 테스트 코드를 작성할 때 사용하는 의존하는 객체의 대역이다. 테스트 대역에는 여러 종류가 있으며, 상황에 따라 적절한 종류를 활용하여 테스트 코드를 작성하면 된다.

DOC (Depended-On-Component)

정의 : 애플리케이션에서 테스트 대상이 실제 의존하는 클래스나 컴포넌트 (= 운영코드)

테스트 환경에서 DOC(운영코드)를 사용하기 어려운 환경에서 타깃 클래스/컴포넌트의 로직만을 검증하기 위해 DOC 인터페이스를 준수하는 테스트 대역을 만들어 사용한다. 참고로 인터페이스의 모든 부분을 구현할 필요는 없고 테스트에서 필요한 기능만 제공해도 된다.

Test Double의 종류와 사용

테스트 대역을 사용하여 테스트를 하면 좋은 상황에 대한 예시는 다음과 같다.

  • SUT이나 DOC가 SUT의 간접출력에 대해 관찰지점을 주지 못하는 상황의 요구사항을 테스트해야하는 경우
  • SUT을 간접입력으로 실행할 수 있게하는 제어지점을 DOC가 제공하지 않는 상황에서 테스트해야하는 경우
  • 느린테스트를 더 빠르고 자주 실행하기 위해 고치는 경우

간접출력

SUT의 API가 아닌 다른 시스템이나 컴포넌트로 출력되는 SUT의 동작을 말한다. 주로 Mock이 간접 출력을 가로채 예상 값과 비교하는 방법으로 관찰지점을 구현한다.
e.g. 다른 컴포넌트 메서드 호출, 메시지채널에서 전송된 메시지, DB에 삽입된 레코드

 

간접입력

다른 컴포넌트에서 출력되는 값이 SUT의 동작에 영향을 주는 동작을 말한다. 주로 stub을 사용하여 SUT에 간접 입력을 SUT에 주입하도록 제어한다.

e.g. 함수의 실제 반환 값, 매개 변수, DOC에 발생한 익셉션

 

관찰지점 (Observation point)

SUT실행 후 상태를 검사하는 방법

 

제어지점 (Control point)

SUT에게 할 일을 요청하는 방법

다시 돌아와서, 테스트 대역은 어떻게 그리고 왜 대역을 쓰는지에 따라 분류된다.

Test Stub

  • control point for the Indirect Inputs of the SUT
  • 테스트 중에 생긴 호출에 대해 미리 준비된 답변만을 제공한다. 
  • SUT이 의존하는 실제 컴포넌트/객체를 교체한다.
  • 간접입력으로 SUT의 기능을 테스트한다.
    • e.g. 정상 메일에 대한 기능 테스트
@Test
public void testSendAll_withMail() {
   List<Mail> mails = Arrays.asList(new Mail("gigi", "hi", "테스트 테스트", "N"), 
                                                            new Mail("gwan-soo", "[긴급]꼭 읽어주세요", "과제 어디까지?", "Y"));
   var mailBox = new MailBoxStub(mails) // stub
   var sut = new MailService(mailBox, messanger);
   boolean isSuccess = sut.sendAll();

   assertThat(isSuccess).isTrue();
}

...

public class MailBoxStub implements MailBox {
    private final List<Mail> mails;

    public MailBoxStub(List mails) {
        this.mails = mails;
    }
    ...
        // MailBox 메서드 구현 (override)
}

Test Spy

  • observation point for the indirect outputs of the SUT
  • SUT 기능 호출 방법에 따라 일부 정보를 기록하는 스텁. (= 메서드 콜이 된 간접출력 대상 클래스)
    • SUT의 간접출력을 검증하는 기능을 가진 Test stub의 확장 버전이다.
    • 해당 객체 (=간접출력이 된 컴포넌트/클래스)내부에서 정보를 기록한다. (= 간접 출력 캡처)
    • 테스트 대상에서는 다시 이 클래스의 메스드를 호출(stub과 같이 외부 -> 테스트 대상으로 데이터 in)하여 검증
    • spy객체는 stub객체처럼 테스트 대상 내부에서 지정된 입력을 주는데 쓰일 수 있임
  • 대개 그저 “기록 기능을 가진 test stub” 이라고 하기도 한다.
  • mock과 같은 목적을 가지고 있지만 사용법이 test stub과 비슷하다.
    • e.g. 메일 서비스에서 발신한 메일 갯수 기록
@Test
public void testTextResult_when_sendAllSuccess() {
   List<Mail> mails = Arrays.asList(new Mail("gigi", "hi", "테스트 테스트", "N"), 
                                                            new Mail("gwan-soo", "[긴급]꼭 읽어주세요", "과제 어디까지?", "Y"));
   var mailBox = new MailBoxStub(mails);
   var messanger = new ResultMessangerSpy(); // spy
   var sut = MailService(mailBox, messanger);
   boolean isSuccess = sut.sendAll();

   assertThat(messanger.getLog()).usingRecursiveFieldByFieldElementComparator().containsAll(mails);
}

...

public class MailBoxStub implements MailBox {
    private final List<Mail> mails;

    public MailBoxStub(List mails) {
        this.mails = mails;
    }
    ...
        // MailBox 메서드 구현 (override)
}

...

// ResultMessange는 문자로 메일 내용을 보내는 역할
// mailService.sendAll이 호출될 때 sendAll 내부에서 ReusltMessanger 로 메일 데이터들을 보냄
public class ResultMessagnerSpy implements ResultMessanger { 
    private final List<Mail> log = new ArraysList<Mail>();

    @Override
    public void upsertMail(Mail mail) {
        log.add(mail);
    }

    public List<Mail> getLog() {
        return log;
    }
}

Mock Object

  • observation point for the indirect outputs of the SUT
  • 예상되는 호출의 스펙들을 기대하는 값으로 프로그래밍 한 후, 예상하는 값을 받았는지 못 받았는지를 검증한다.
  • 동작을 검증하는 것이 핵심 - stub과 구별되는 지점.

Fake Object

  • SUT의 간접입력, 출력 모두 검증할 때 사용한다.
  • 실제 작동하는 작은 DOC 구현
  • 운영 코드를 쓰기 부담스럽거나 느릴 때 Fake Object를 사용한다.
    • e.g. 인메모리 데이터베이스

Dummy Object

  • SUT 기능 메서드에 전달은 되지만, 필요없거나 사용하지 않는 객체
  • 주로 메서드의 매개변수를 채우는 데만 사용된다.

게임에서의 비유

 

Test Stub : 야~ 캐릭터 생명이 0이야. 그러면 공격할 수 없니? | DI하는 요소에서 테스트로 흘러 들어가는 입력을 내가 조작. 그에 대한 반응이 예상하는 결과 데이터인지 검증


Test Spy : 야~ 캐릭터가 공격 3번 받았어. 체력 깎일 때 공격 기록할 거임. 3번 받았다고 하니? | 테스트 대상 내부에서 DI하는 요소의 메서드를 호출할 때 이를 기록하여 확인. 이 정보는 다시 테스트에서(데이터 흐름 : DI -> 테스트 내부) 예상하는 결과와 맞는지 검증함.

 

mock : 야~ 캐릭터가 방탄조끼 착용하면 들어온 공격은 무력화 돼. 공격이 들어왔을 때 체력바가 줄어드는 프로세스는 동작 안 했지? | 테스트 대상에 입력이나 결과를 내가 조작. 그랬을 때 반응이나, 동작이 예상하는대로인지를 검증

 

Fake : 야~캐릭터가 보유한 캐시는 실물이니까 쓰면 클난다. 가상의 캐시 저장소 만들어주자. 캐시 잘 빠져나가니? | 예상되는 결과 검증


Dummy : 야~ 대화방에서는 어떤 걸 할 수 있는지 보고 싶어. 캐릭터가 가지는 펫이 아무런 일도 안하는데 캐릭터랑 붙어다니니 그냥 같이 넣어주자. 

 

주의점

테스트 대역을 사용하면 테스트 작성 코드에 굉장히 많은 도움을 받을 수 있다. 그럼에도 테스트 대역을 사용하는데 몇가지 주의할 점이 있다.

먼저 테스트 대역 없이도 동작을 검증할 수 있는 테스트를 최소한 하나를 갖게 해야한다. 이는 실제 운영 환경과 테스트 환경은 엄연히 다르기 때문이다.

두 번째로 검증 대상인 SUT의 부분들(의존성 아님)을 대역으로 교체하면 않도록 주의해야한다. 왜냐하면 잘못된 소프트웨어를 테스트하는 테스트가 될 수 있기 때문이다.

마지막으로 테스트 대역을 과하게 사용할 경우 테스트가 너무 많은 구현, 명세에 대해 알게 될 수도 있다. 따라서 적절하게 테스트 대역을 사용하는 것이 좋다.

 

 

참고 자료

https://martinfowler.com/bliki/TestDouble.html

https://fathinah.medium.com/learn-the-art-of-faking-with-mock-and-stub-example-in-react-139d472dc3a1

http://xunitpatterns.com/Test Double.html

https://tecoble.techcourse.co.kr/post/2020-09-19-what-is-test-double/