Junit in Action 3판 - 8장) 모의 객체로 테스트하기
본장에서는 테스트케이스 작성시, 다른 클래스의 의존하는 코드를 테스트하는 방법중인 2번째 방법인 '모의 객체'에 관하여 설명한다.
DB접속, HTTP 연계 등 실제 준비가 되어있지 않은 의존성을 테스트하기 위해 실제 객체를 구현하여 테스트를 대체하는 스텁과는 달리 모의 객체는 실제 객체를 구현하지않고 결과값만 사전에 정의한대로 반환하는 껍데기를 제공한다.
모의 객체란 무엇인가
모의 객체란 실제 객체가 아닌 껍데기 객체를 의미한다.
즉, 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 흉내 내는 가짜 모듈을 작성하여 테스트의 효용성을 높이는데 사용하는 객체이다.
예를 들어, DB접속 등의 3rd party에 의존하는 코드를 테스트할시 실제 DB를 연결하지않고 해당 코드를 모의객체로 전환하여 사전에 정의한 결과값을 반환하도록 하여 실제 DB가 연결된것을 흉내내준다.
모의 객체를 사용했을 때의 가장 큰 장점은 메서드에 집중하는 테스트를 만들 수 있다는 점이다. 모의 객체를 사용하면 테스트 대상 메서드가 다른 객체를 호출해서 발생하는 부수효과가 전혀 없기 때문이다.
HTTP 연결 모의하기
모의 객체가 어떻게 작동하는지 살펴보기 위해 웹 서버에 HTTP연결을 맺고 페이지 콘텐츠를 읽어들이는 코드를 테스트한다.
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 (-1 != (count = is.read(buffer))) {
content.append(new String(buffer, 0, count));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return content.toString();
}
}
해당 코드를 모의객체로 테스트하려는데 문제가 있다.
실제 http연결이 아닌 모의객체 연결로 변경하기 위해서는 openConnection()를 조작해야한다. 그러려면 url를 Url 클래스를 상속하는 모의 객체를 넣어야한다. 하지만 Url 클래스는 final이므로 상속할 수 없다. 또한 해당 코드는 http 연결만 가능하도록 하드코딩이 되어있으므로 추후에 tcp등의 확장이 불가능하다.
이때 리팩토링을 시도해야한다.
테스트를 위해서 코드를 리팩토링 하는게 맞을까? 라는 의문이 드는것은 당연하다. 하지만 코드가 테스트하기에 충분히 유연하지 않다면 코드를 수정하는 것은 당연하다. 대상 코드의 리팩토링을 유도하는 것도 테스트의 순기능 중의 하나라는 것을 잊지말자.
Url클래스를 상속할 수 없으니 그 상위의 커넥션 자체를 인터페이스로 만들고, 해당 인터페이스를 구현하는 mock클래스와 http클래스를 만든다. 이렇게 하면 추후에 tcp연결 시에도 tcp클래스를 확장하기만 하면된다.
// 인터페이스
public interface ConnectionFactory {
InputStream getData() throws Exception;
}
// 모의 객체
public class MockConnectionFactory implements ConnectionFactory {
private InputStream inputStream;
public void setData(InputStream stream) {
this.inputStream = stream;
}
public InputStream getData() throws Exception {
return inputStream;
}
}
// 실제 객체
public class HttpURLConnectionFactory implements ConnectionFactory {
private URL url;
public HttpURLConnectionFactory(URL url) {
this.url = url;
}
public InputStream getData() throws Exception {
HttpURLConnection connection = (HttpURLConnection) this.url.openConnection();
return connection.getInputStream();
}
}
getContent를 호출한 쪽으로 부터 ConnectionFactory를 받아서 getData()를 실행한다.
이렇게 리팩토링 되었을때 넘겨받은 인자값을 실행만 하게되므로 소스가 수정되지않아도 된다.
호출한쪽이 알맞는 인자값을 잘 넣어줄 책임이 있는 IOC 제어의 역전이 일어난다.
public class WebClient {
public String getContent(ConnectionFactory connectionFactory) {
String workingContent;
StringBuffer content = new StringBuffer();
try (InputStream is = connectionFactory.getData()) {
int count;
while (-1 != (count = is.read())) {
content.append(new String(Character.toChars(count)));
}
workingContent = content.toString();
} catch (Exception e) {
workingContent = null;
}
return workingContent;
}
}
테스트 코드는 아래와 같다.
리팩토링을 통하여 테스트시에 모의객체 주입을 하기만하면, 실제 http연결을 하지않고 모의객체가 실행된다.
public class TestWebClient {
@Test
public void testGetContentOk() throws Exception {
MockConnectionFactory mockConnectionFactory = new MockConnectionFactory();
MockInputStream mockStream = new MockInputStream();
mockStream.setBuffer("It works");
mockConnectionFactory.setData(mockStream);
WebClient client = new WebClient();
String workingContent = client.getContent(mockConnectionFactory); // 모의 객체 주입
assertEquals("It works", workingContent);
mockStream.verify();
}
}
모의 객체 프레임워크 사용해보기
지금까지는 모의객체가 무엇이고 어떻게 사용하는지 알아보았다. 모의객체는 테스트하기에는 편하지만 결국 모의객체를 밑바닥부터 직접 구현하는것이 번거로웠다. 하지만, 매 테스트시마다 모의객체 클래스를 직접 구현할 필요는 없다. 모의 객체를 더 쉽게 만들 수 있도록 지원하는 프레임워크가 존재하기 때문이다.
모의객체 프레임워크는 여러가지가 있는데 크게 3가지로 나뉜다.
- EasyMock
- JMock
- Mockito
각자의 장단점이 있지만, 현재 가장 인기가 있는 모의 객체 프레임워크인 'Mockito'를 설명한다.
Mockito를 사용하기 위해서는 의존성을 추가해야한다.
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.2.4</version>
<scope>test</scope>
</dependency>
Mockito를 사용하면 mock객체를 간단히 만들 수 있다.
mock객체를 생성하기 위해 위처럼 클래스를 직접 구현할 필요가 없다.
리턴값 모킹시에는 when(목객체.메서드).thenRetrun(결과값)으로 하면된다.
위의 코드를 Mockito로 사용하여 다시 구성해보았다.
@ExtendWith(MockitoExtension.class) // @Mock어노테이션 사용시 필수
public class TestWebClient {
@Mock
private ConnectionFactory factory;
@Test
public void testGetContentOk() throws Exception {
// given
given(factory.getData()).willReturn("It works"); // getData() 실행시 결과값 모킹
// when
WebClient client = new WebClient();
String workingContent = client.getContent(factory); // 모의 객체 주입
// then
assertThat(workingContent).isEqualTo("It works");
verify(() -> { // 실제 해당 데이터가 1번 실행되었는지도 검증가능
factory.getData();
}, times(1));
}
}