Java와 Junit을 활용한 실용주의 단위 테스트 - Mock테스트 1편

2021, Feb 20    

몇개월 전 진행 중이던 프로젝트를 주기적으로 테스트 하는 CI환경을 구성하다 보니,
mock테스트를 해야 겠다는 생각이 들었다.
구글링을 열시미 했으나, 막연하게만 다가와서 책을 통해 개념을 좀 더 알아 볼 필요성을 느꼈고, 찾아 보니 좋은 책이 있어서 공부한 부분 정리해 본다.

stub을 사용한 테스트

다른 서비스의 url을 호출 하는 테스트를 실제 호출 하는 테스트 코드의 단점

  • 코드만 테스트하는 것에 비해 속도가 느리다.
  • 테스트하려고 하는 API 가 항상 응답이 가능한 상태인지 보장 할수 없다. 통제 밖이다.
  • 테스트 하는 환경(CI를 통한 다른 서버에서 실행하는 경우)에서 테스트 하려고 하는 API가 호출 가능한 상태로 만들어 줘야 함

단위 테스트의 목적이 무엇인지 집중해 보면

  • API를 호출 하여 처리하는 내 코드를 테스트 하고 싶은 것이지, 외부 서비스까지 테스트 하려는것은 아니다.
  • 의존성이 있는 다른 코드와 분리하여 내가 짠 메서드의 로직에 관한 단위 테스트를 원함.
  • API를 호출하는 코드, HTTP 응답에서 생성되는 객체를 생성하는 로직을 테스트에 집중하자.

번거고운 동작을 스텁으로 대체

  • 응답 처리에 대한 로직에만 검증에만 집중하려면 테스트를 위한 응답 JSON을 하드 코딩 하여 응답 받을 수 있도록 테스트 상황을 만든다.
  • 이렇게 테스트 용도로 하드 코딩한 응답값을 반환하도록 만들어진 코드(구현체)를 스텁(stub)이라고 한다.
  • stub 코드 예)
    //http api를 호출 하는 원본 코드
    public class HttpImpl implements Http {
      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();
          }
      }
    }
    
    //하드코딩 된 json 값을 응답해주는 스텁 코드
    Http http = (String url) -> 
         "{\"address\":{"
         + "\"house_number\":\"324\","
         + "\"road\":\"North Tejon Street\","
         + "\"city\":\"Colorado Springs\","
         + "\"state\":\"Colorado\","
         + "\"postcode\":\"80903\","
         + "\"country_code\":\"us\"}"
         + "}";
    
  • 그러면 이 stub을 어떻게 사용할 수 있을까? 의존성 주입을 이용한다.
    보통 HttpImpl를 사용하는 클래스를 구현할때 다음과 같이 HttpImpl객체를 주입을 통해 사용하도록 구현되어 있을 것 이다.
     public class AddressRetriever {
        private Http http;
    
        public AddressRetriever(Http http) {
            this.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);
            // ...
        }
     }
    
  • 테스트 코드에서는 원래 origin 객체인 HttpImpl대신 stub 객체를 주입 시켜준다.
    Http http = (String url) -> 
         "{\"address\":{"
         + "\"house_number\":\"324\","
         + "\"road\":\"North Tejon Street\","
         + "\"city\":\"Colorado Springs\","
         + "\"state\":\"Colorado\","
         + "\"postcode\":\"80903\","
         + "\"country_code\":\"us\"}"
         + "}";
      AddressRetriever retriever = new AddressRetriever(http);
    
      Address address = retriever.retrieve(38.0,-104.0);
    

스텁에 기능 추가하기

  • 스텁을 http 통신과 같은 외부 환경에 대한 영향을 받지 않고 검증하고자 하는 코드만 테스트 할수 있게 되었다.
  • 하지만 예제의 스텁 http코드는 get() 메소드에 넘겨진 어떤 값에도 무관하게 항상 정상적인 JSON을 응답함하게 된다. httpImpl.get() 메소드의 인자값에 대한 검증도 테스트 코드에 추가되는 것이 좋을 것이다.
  • 스텁 http get메소드에 전달되는 인자에 대한 검증 코드를 추가하여 하면 보다 실제 서비스 상황에서 발생할 수 있는 케이스도 테스트 해 볼 수 있다.
  Http http = (String url) -> 
        { 
          //스텁 코드에 http에 전달 되는 파라미터 인증 로직 추가
          if (!url.contains("lat=38.000000&lon=-104.000000")) 
            fail("url " + url + " does not contain correct parms");
            
            .....
        }

개인 생각 정리

  • mock테스트는 이렇게 stuck코드를 통해서 내가 테스트 하고자 하는 목적의 코드를 검증할 수 있다는 개념이다.
  • 예제를 보면 쉽게 보이지만, 사실 실제 웹 어플리케이션에서 코드 검증을 위한 stub을 다 만들어 내는 것은 좀처럼 쉬운 것은 아닌 것 같다.
  • 지금은 http 에 대한 stub만 구현 하였으나, 보통의 웹서비스에서는 연결 되어 있는 외부 인프라가 많아(mysql, elasticsearch, redis 등) 개인적인 경험으로는 노가다성이었던 것으로 기억한다.
  • 이런 부분에 대해서 좀더 이 책을 공부를 하여 보안점을 찾아 봐야 겠다.