ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JUnit] 스텁을 활용한 테스트
    DEV/JUnit 2024. 7. 14. 23:26

    애플리케이션을 개발하다보면 몇몇 코드가 다른 클래스나 외부 환경에 의존하는 경우가 있다.

    특정한 런타임 환경에 의존하는 애플리케이션을 위한 단위 테스트를 작성하기는 매우 어렵다.

     

    실제로 필요한 환경을 테스트의 일부로 구성하여 테스트를 수행하는것은 효과적이나 이는 항상 가능한 방법이 아니다.

    실제 장비 지원 없이도 소스 코드에 대한 테스트를 지속적으로 작성하고 실행할 수 있도록 가짜 객체를 활용해야 하는데, 방법은 스텁과 모의객체를 활용하는 것 두 가지가 있다.

     

    이번 글에서는 스텁을 활용한 방법을, 다음 글에서는 모의 객체를 활용하는 방법을 정리하고자한다.

     

    스텁

    호출자를 실제 구현 코드에서부터 격리하기 위해 실제 코드 대신 런타임에 동작하는 코드를 말한다. 단순하게 만든 스텁으로 실제 코드의 복잡한 기능을 대체하면 애플리케이션에 독립적으로 테스트를 수행할 수 있다.

     

    즉, 실제 코드 혹은 아직 구현되지 않은 코드의 동작을 가장하기 위한 장치로 시스템의 일부를 사용할 수 없는 상황에서 테스트하기위해 스텁을 사용한다.

     

    스텁을 활용하기 좋은 경우

    • 기존 시스템이 너무 복잡하고 깨지기 쉬워 수정이 어려울 때
    • 소스 코드가 통제할 수 없는 외부 환경에 의존하고 있을 때
    • 파일 시스템, 서버, 데이터베이스같은 외부 시스템을 완전히 교체해야 할 때
    • 하위 시스템간 통합 테스트 같은 거친 테스트를 수행해야 할 때

    거친 테스트?

    : 테스트의 단위가 비교적 크다는 의미로 쓰였다.

    책에서 원어 coarse-grained가 결이 거친, 성긴 이라는 의미를 가져 '거친 테스트'로 번역하여 옮겨졌다.

     

    소프트웨어 개발에서 비교적 큰 단위의 대상을 표현할 때 쓰여 여러 컴포넌트 혹은 시스템의 상당 부분을 의미한다.

     

    스텁을 활용하기 어려운 경우

    • 실패의 원인을 밝힐 수 있는 정확한 에러 메시지를 확인하기 위해 세밀한 테스트가 필요할 때
    • 코드 전체가 아닌 일부분만 격리해 테스트를 수행해야 할 때

    이런 상황에서는 스텁보다 모의객체를 사용하는 것이 좋다.

     

    스텁을 활용하면 테스트 대상 객체를 수정하지 않으면서도 실제 운영에서 실행되는 것과 동일한 소스를 테스트할 수 있다는 장점이 있다. 단점은 작성하기가 까다로운데, 가정해야 하는 시스템이 복잡할 때는 더욱 그러하다.

     

    - 작성하기가 까다로워 스텁 자체를 디버깅해야 하는 일이 종종 생긴다.

    - 스텁이 복잡해져 유지보수하기가 어려울 수 있다.

    - 세밀한 단위 테스트에 적합하지 않을 수 있다.

    - 테스트에 따라 다른 스텁을 만들어야 할 수도 있다.

     

    따라서 비즈니스 로직이 복잡하다면 스텁은 적합하지 않다.

     

    스텁으로 HTTP 연결 테스트하기

    특정 URL에 대한 HTTP 연결을 맺은 다음 해당 URL에서 제공하는 리소스를 읽어 들이는 코드

    import java.io.IOException;
    import java.io.InputStream;
    import java.net.HttpURLConnection;
    import java.net.URL;
    
    public class WebClient {
        public String getContent(URL url) {
            StringBuffer content = new StringBuffer();
            
            try {
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setDoInput(true);
    
                InputStream is = connection.getInputStream();
                byte[] buffer = new byte[2048];
                int count;
                while ((count = is.read(buffer)) != -1) {
                    content.append(new String(buffer, 0, count));
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            
            return content.toString();
        }
    }

     

    이 WebClient 클래스를 단위 테스트하기 위해서는 서버가 필요하다.

    이때 상대적으로 쉬운 해결책은 스텁으로 사용할 아파치 테스트 서버를 설치하고 테스트 서버에서 보여줄 간단한 웹페이지를 만드는 것이다. 이러한 방법은 전형적이고 일반적으로 자주 사용되지만, 다음과 같은 단점이 있다.

     

    1. 환경에 의존한다. => 테스트 시작전 전체 환겨잉 구성되어 있고 실행 중인지 확인해야함

    2. 테스트 로직이 분리되어 있다. =>테스트 케이스와 테스트 웹페이지 두 곳으로 흩어짐

    3. 테스틀 자동화하기 어렵다. => 웹 페이지를 서버에 배포하고 기동한 다음 단위테스트를 실행해야함

     

    쉬운 해결책으로 내장 웹서버를 사용하면 된다. 책에서는 Jetty를 스텁으로 만드는 예시를 보여주고 있으나 전체 웹 서버를 스텁으로 만들어 복잡하고, 스텁을 디버깅해야하는 단점이 있다.

     

    이럴 때는 HttpURLConnection 클래스를 스텁으로, 즉 HTTP 연결만 스텁으로 만드는 방법이 있다.

    웹 리소스를 스텁으로 만드는 것보다 가볍게 만들 수 있다.

     

    사실상 HTTP연결은 테스트할 수 없지만, 코드를 격리하여 테스트하는 것은 중요한 사항이며 위 테스트는 HTTP연결 테스트가 주된 목적이 아니기 때문에 상관이 없다. HTTP 연결은 기능 테스트나 통합 테스트로 차후에 테스트 할 수 있다.

     

    테스트를 위한 스트림 핸들러 스텁 클래스

    import org.junit.jupiter.api.BeforeAll;
    import org.junit.jupiter.api.Test;
    
    import java.net.*;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    public class TestWebClient {
        @BeforeAll
        public static void setUp() {
        	//스텁으로 사용할 Factory객체 설정
            URL.setURLStreamHandlerFactory(new StubStreamHandlerFactory());
        }
    	
        //StubHttpURLStreamHandler 클래스를 사용하기 위해 내부클래스 구현
        private static class StubStreamHandlerFactory implements URLStreamHandlerFactory {
            @Override
            public URLStreamHandler createURLStreamHandler(String protocol) {
                return new StubHttpURLStreamHandler();
            }
        }
    	
        //StubHttpURLStreamHandler 클래스를 사용하기 위해 내부클래스 구현
        private static class StubHttpURLStreamHandler extends URLStreamHandler {
            @Override
            protected URLConnection openConnection(URL url) {
                return new StubHttpURLConnection(url);
            }
        }
    
        @Test
        public void testGetContentOk() throws MalformedURLException {
            WebClient client = new WebClient();
            String workingContent = client.getContent(new URL("http://localhost/"));
            assertEquals("It works", workingContent);
        }
    }

     

    HttpURLConnection 스텁 클래스

    import java.net.HttpURLConnection;
    import java.net.ProtocolException;
    import java.net.URL;
    import java.io.InputStream;
    import java.io.IOException;
    import java.io.ByteArrayInputStream;
    
    public class StubHttpURLConnection extends HttpURLConnection {
        private boolean isInput = true;
    
        protected StubHttpURLConnection(URL url) {
            super(url);
        }
    	
        //테스트 대상 메서드 재정의
        @Override
        public InputStream getInputStream() throws IOException {
            if (!isInput) {
                throw new ProtocolException("Cannot read from URLConnection" + " if doInput=false (call setDoInput(true))");
            }
            ByteArrayInputStream readStream = new ByteArrayInputStream(new String("It works").getBytes());
            return readStream;
        }
    
        @Override
        public void connect() throws IOException {
        }
    
        @Override
        public void disconnect() {
        }
    
        @Override
        public boolean usingProxy() {
            return false;
        }
    }

     

    HttpURLConnection 추상클래스를 상속하여 스텁으로 사용할 수 있게 메서드를 재정의한다.

    테스트에서 원하는 값을 반환하기 위해 "It works" 문자열을 스트림으로 만들어 호출자에게 반환하는 스텁을 만든다.

     

    결론적으로 웹 리소스를 스텁으로 만드는 것보다 HTTP연결을 스텁으로 만드는 것이 쉽다.

    HTTP 연결을 스텁으로 만들면 통합테스트는 할 수 없으나 WebClient의 비즈니스 로직에 대한 단위 테스트를 더 쉽게 만들 수 있다.

     

     

     

    댓글

Designed by Tistory.