본문 바로가기
테스트

번거로운 동작을 스텁(stub)으로 대체

by 이상한나라의개발자 2024. 1. 19.

"자바와 Junit를 활용한 실용주의 단위 테스트" 책에 나오는 번거로운 동작을 스텁으로 대체에 대해 알아볼까 합니다.

단위 테스트에서 외부 시스템이나 서비스와의 상호작용을 모의 객체(stub)를 사용하여 단순화 하는 방법에 대해 설명하는데요

이 접근법은 테스트의 실행 속도를 높이고, 외부 시스템의 불안정성이나 제어 불가능한 요소들로 부터 독립적인 테스트 환경을 만드는데 도움을 줍니다.

 

* 테스트 용도로 하드 코딩한 값을 반환하는 구현체를 스텁이라고 합니다.

 

번거로운 동작 예시

  • 네트워크를 통한 데이터 통신 : 실제 서버에 접속하여 데이터를 주고 받는 과정은 네트워크 지연, 서버 문제 등으로 테스트가 느려지가나 실패할 수 있습니다.
  • 데이터베이스 접근 : 실제 데이터베이스에 접근하는 것은 테스트 실행 속도를 늦출 뿐만 아니라, 데이터베이스 상태에 따라 테스트 결과가 달라질 수 있습니다.
  • 파일 시스템 접근 : 파일 시스템에 읽기/쓰기를 하는 것은 테스트 환경에 따라 다른 결과를 초래할 수 있습니다.

 

스텁(stub)의 사용

스텁은 이러한 번거로운 동작을 대체하기 위해 사용됩니다. 스텁은 테스트 대상 객체가 의존하는 부분을 간단한 구현으로, 예측이 가능한 결과를 반환하거나 특정 동작을 수행합니다. 이를 통해 실제 복잡한 로직이나 외부 시스템과의 상호 작용을 모방할 수 있습니다.

 

아래 코드를 보면 Http를 구현한 HttpImpl은 외부 라이브러리를 의존하고 있으며 해당 코드는 이미 다른 많은 시스템에서 성공적으로 배포되서 활용되고 있기 때문에 테스트 작성 여부를 고민할 필요가 없습니다. 또한, 해당 라이브러리의 동작 여부는 우리의 제어권 밖에 있죠.

아래 코드를 그대로 테스트 하게 되면 문제점이 생기는데요

 

  • 실제 호출에 대한 테스트는 나머지 대다수의 빠른 테스트들에 비해 속도가 느릴 것입니다.
  • Nominatim Http API가 항상 가용한지 보장할 수 없습니다. 우리의 제어권 밖에 있습니다.
@FunctionalInterface
public interface Http {

    String get(String url) throws IOException;
}


public class HttpImpl implements Http {

    @Override
    public String get(String url) throws IOException {

        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet request = new HttpGet(url);
        CloseableHttpResponse response = client.execute(request);
        try {
            HttpEntity entity = response.getEntity();
            return EntityUtils.toString(entity);
        } finally {
            response.close();
        }
    }
}


@RequiredArgsConstructor
public class AddressRetriever {
   private final Http http;

   public Address retrieve(double latitude, double longitude)
         throws IOException, ParseException {
      String parms = String.format("lat=%.6flon=%.6f", latitude, longitude);
      String response = http.get("http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"+ parms);

      JSONObject obj = (JSONObject)new JSONParser().parse(response);
      // ...

      JSONObject address = (JSONObject)obj.get("address");
      String country = (String)address.get("country_code");
      if (!country.equals("us"))
         throw new UnsupportedOperationException(
               "cannot support non-US addresses at this time");

      String houseNumber = (String)address.get("house_number");
      String road = (String)address.get("road");
      String city = (String)address.get("city");
      String state = (String)address.get("state");
      String zip = (String)address.get("postcode");
      return new Address(houseNumber, road, city, state, zip);
   }
}

 

그러므로, 의존성이 있는 다른 코드와 분리하여 retrieve() 메서드의 로직에 단위 테스트를 진행합니다. 이때 Http 통신에서 응답하는 데이터를 스텁으로 대체합니다.

 

 

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class AddressRetrieverTest {

    @Test
    void 유효한_좌표에_대한_적절한_주소에_대한_답변() throws IOException, ParseException {

        // given (준비)
        // 테스트 용도로 하드 코딩한 값을 반환하는 구현체를 스텁이라고 한다(stub)
        // 아래는 람다를 활용한 스텁 구현 방법이다.
        Http http = url ->  "{\"address\":{"
                + "\"house_number\":\"324\","
                + "\"road\":\"North Tejon Street\","
                + "\"city\":\"Colorado Springs\","
                + "\"state\":\"Colorado\","
                + "\"postcode\":\"80903\","
                + "\"country_code\":\"us\"}"
                + "}";

        // when (실행)
        AddressRetriever retriever = new AddressRetriever(http);
        Address address = retriever.retrieve(38.0,-104.0);

        // then (단언 assert)
        assertThat(address.houseNumber).isEqualTo("324");
        assertThat(address.road).isEqualTo("North Tejon Street");
        assertThat(address.city).isEqualTo("Colorado Springs");
        assertThat(address.state).isEqualTo("Colorado");
        assertThat(address.zip).isEqualTo("80903");
    }

 

위 코드에서는 람다식을 사용하여 Http 메서드를 구현 하였습니다.  이를 AddressRetriever 클래스의 생성자를 통해 주입을 하고 테스트를 진행하게 됩니다. 이렇게 되면, 우리는 실제 HttpImpl 의존에서 벗어나 테스트를 가볍게 진행할 수 있게 됩니다. 이를 인터페이스를 통한 의존 역전이라고 합니다.

 

다른 방식으로 실제 테스트 구현체를 만들어서 테스트 하겠습니다.

public class HttpImplTest implements Http {
    @Override
    public String get(String url) throws IOException {
        return "{\"address\":{"
                + "\"house_number\":\"324\","
                + "\"road\":\"North Tejon Street\","
                + "\"city\":\"Colorado Springs\","
                + "\"state\":\"Colorado\","
                + "\"postcode\":\"80903\","
                + "\"country_code\":\"us\"}"
                + "}";
    }
}


    @Test
    void 유효한_좌표에_대한_적절한_주소에_대한_답변_테스트_구현체_의존_주입() throws IOException, ParseException {

        // given (준비)
        Http http = new HttpImplTest();

        // when (실행)
        AddressRetriever retriever = new AddressRetriever(http);
        Address address = retriever.retrieve(38.0,-104.0);

        // then (단언 assert)
        assertThat(address.houseNumber).isEqualTo("324");
        assertThat(address.road).isEqualTo("North Tejon Street");
        assertThat(address.city).isEqualTo("Colorado Springs");
        assertThat(address.state).isEqualTo("Colorado");
        assertThat(address.zip).isEqualTo("80903");
    }

 

이 예시에서 Http 인터페이스의 스텁 구현은 실제 네트워크를 통해 가져오는 대신, 하드 코딩된 값을 반환합니다. 이를 통해 AddressRetriever 클래스의 retrieve 메서드는 실제 외부 서비스와의 의존성 없이 테스트할 수 있습니다.

'테스트' 카테고리의 다른 글

테스트 대역 5가지  (0) 2024.07.17
테스트  (0) 2024.07.16
Mockito 를 활용한 단위 테스트  (0) 2024.01.24
번거로운 동작을 스텁(stub)으로 대체  (0) 2024.01.24
JUnit 기본  (2) 2024.01.03