[JUnit] 모의 객체 프레임워크 EasyMock / JMock / Mockito
모의 객체를 사용하기 위한 유용한 클래스를 제공하는 오픈 소스 프레임워크
- EasyMock
- JMock
- Mockito
EasyMock
maven
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>5.4.0</version>
</dependency>
gradle
testImplementation group: 'org.easymock', name: 'easymock', version: '5.4.0'
EasyMock을 활용한 TestAccountService
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.easymock.EasyMock.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestAccountServiceEasyMock {
// 모의하려는 객체를 인스턴스 변수로 선언. EasyMock 프레임워크는 인터페이스만 모의 가능
private AccountManager mockAccountManager;
@BeforeEach
public void setUp() {
// createMock을 통해 원하는 클래스의 모의 객체 생성
mockAccountManager = createMock("mockAccountManager", AccountManager.class);
}
@Test
public void testTransferOk() {
//given
Account senderAccount = new Account("1", 200L);
Account beneficiaryAccount = new Account("2", 100L);
// 기대를 정의한다.
mockAccountManager.updateAccount(senderAccount);
mockAccountManager.updateAccount(beneficiaryAccount);
expect(mockAccountManager.findAccountForUser("1"))
.andReturn(senderAccount);
expect(mockAccountManager.findAccountForUser("2"))
.andReturn(beneficiaryAccount);
// 기대 정의가 끝나면 replay를 호출한다.
replay(mockAccountManager);
AccountService accountService = new AccountService();
accountService.setAccountManager(mockAccountManager);
//when
accountService.transfer("1","2", 50L);
//then
assertEquals(150L, senderAccount.getBalance());
assertEquals(150L, beneficiaryAccount.getBalance());
}
@AfterEach
public void tearDown() {
verify(mockAccountManager);
}
}
EasyMock을 사용할 때는 두가지 방법으로 기대를 선언할 수 있다.
메서드 반환 타입이 void인 경우 모의 객체에서 간단하게 호출 할 수 있다.
mockAccountManager.updateAccount(senderAccount);
mockAccountManager.updateAccount(beneficiaryAccount);
메서드가 어떤 종류든 객체를 반환 할 때 EasyMock API인 expect나 andReturn 메서드를 사용한다.
expect(mockAccountManager.findAccountForUser("1"))
.andReturn(senderAccount);
expect(mockAccountManager.findAccountForUser("2"))
.andReturn(beneficiaryAccount);
기대를 선언한 다음에는 replay 메서드를 호출한다. replay 메서드를 호출하면 모의 객체의 행동을 기록하는 단계에서 모의 객체의 동작을 활성화하는 단계로 넘어간다.
replay(mockAccountManager);
단순히 모의 객체의 행동을 기록하는 것만으로는 모의 객체가 동작하지 않는다.
replay 메서드를 호출해서 활성화시켜야 모의객체가 기대한대로 동작한다.
이후로는 동일하게 테스트 하고싶은 transfer 메서드를 호출하고, 예상 결과를 단언한다.
@Test가 실행된 다음 @AfterEach 메서드에서 기대에 대한 검증을 수행한다.
@AfterEach
public void tearDown() {
verify(mockAccountManager);
}
EasyMock을 사용하면 어떤 모의 객체든 verify 메서드를 호출하여 이전에 선언했던 메서드 호출에 대한 기대가 충족되었는지 검증할 수 있다.
EasyMock을 활용한 WebClient
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.easymock.EasyMock.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class TestWebClientEasyMock {
// 모의 객체 선언
private ConnectionFactory factory;
private InputStream stream;
@BeforeEach
public void setUp() {
// 모의 객체 초기화
factory = createMock("factory", ConnectionFactory.class);
stream = createMock("stream", InputStream.class);
}
@Test
public void testGetContentOk() throws Exception {
// 기대 정의
expect(factory.getData()).andReturn(stream);
expect(stream.read()).andReturn(Integer.valueOf((byte)'W'));
expect(stream.read()).andReturn(Integer.valueOf((byte)'o'));
expect(stream.read()).andReturn(Integer.valueOf((byte)'r'));
expect(stream.read()).andReturn(Integer.valueOf((byte)'k'));
expect(stream.read()).andReturn(Integer.valueOf((byte)'s'));
expect(stream.read()).andReturn(Integer.valueOf((byte)'!'));
expect(stream.read()).andReturn(-1);
stream.close();
replay(factory);
replay(stream);
WebClient2 client = new WebClient2();
String workingContent = client.getContent(factory);
assertEquals("Works!", workingContent);
}
@Test
public void testGetContentCannotCloseInputStream() throws Exception {
expect(factory.getData()).andReturn(stream);
expect(stream.read()).andReturn(-1);
stream.close();
expectLastCall().andThrow(new IOException("cannot close"));
replay(factory);
replay(stream);
WebClient2 client = new WebClient2();
String workingContent = client.getContent(factory);
assertNull(workingContent);
}
@AfterEach
public void tearDown() {
verify(factory);
verify(stream);
}
}
EasyMock을 활용하여 getContent가 정상적으로 수행 됐을때의 테스트와 InputStream을 닫을 수 없을 때의 조건을 모사하는 테스트를 작성해보았다.
JMock
Maven
<dependency>
<groupId>org.jmock</groupId>
<artifactId>jmock-junit5</artifactId>
<version>2.13.1</version>
</dependency>
Gradle
testImplementation group: 'org.jmock', name: 'jmock-junit5', version: '2.13.1'
JMock을 활용한 TestAccountService
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.junit5.JUnit5Mockery;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestAccountServiceJMock {
// 프로그래밍 방식 확장 등록
@RegisterExtension
Mockery context = new JUnit5Mockery();
private AccountManager mockAccountManager;
@BeforeEach
public void setUp() {
// context를 이용해 프로그래밍 방식으로 모의객체를 생성
mockAccountManager = context.mock(AccountManager.class);
}
@Test
public void testTransferOk() {
//given
Account senderAccount = new Account("1", 200L);
Account beneficiaryAccount = new Account("2", 100L);
// 기대 선언
context.checking(new Expectations() {
{
oneOf(mockAccountManager).findAccountForUser("1");
will(returnValue(senderAccount));
oneOf(mockAccountManager).findAccountForUser("2");
will(returnValue(beneficiaryAccount));
oneOf(mockAccountManager).updateAccount(senderAccount);
oneOf(mockAccountManager).updateAccount(beneficiaryAccount);
}
});
AccountService accountService = new AccountService();
accountService.setAccountManager(mockAccountManager);
//when
accountService.transfer("1","2", 50L);
//then
assertEquals(150L, senderAccount.getBalance());
assertEquals(150L, beneficiaryAccount.getBalance());
}
}
기대 선언시 다음과 같은 문법을 사용한다.
invocation-count(mock-object).method(argument-constraints);
inSequence(sequence-name);
when(state-machine.is(state-name);
will(action);
then(state-machine.is(new-state-name));
모든 절은 invocation-count(mock-object) 이 부분을 제외하고는 선택적으로 작성할 수 있다.
몇 번 호출했고, 어떤 객체를 호출했는지 구체적으로 적을 수 있다.
이후 메서드가 객체를 반환하면 반환할 객체를 will(returnValue()) 메서드를 호출하여 선언할 수 있다.
호출 횟수를 검증하는 것은 어떻게 되었을까?
EasyMock의 경우 특정 메서드가 기대하는 횟수만큼 호출되었는지 검증해야한다.
JMock은 확장이 이 작업을 대신 처리하여 EasyMock처럼 할 필요가 없다.
만약 메서드를 기대한만큼 호출하지 않으면 테스트가 실패한다.
JMock을 활용한 TestWebClient
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.imposters.ByteBuddyClassImposteriser;
import org.jmock.junit5.JUnit5Mockery;
import org.junit.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class TestWebClientJMock {
@RegisterExtension
Mockery context = new JUnit5Mockery() {
{ // JMock에서 인터페이스가 아닌 클래스에 대한 모의 객체를 생성해야할 때 imposteriser 속성을 정의해야한다.
setImposteriser(ByteBuddyClassImposteriser.INSTANCE);
}
};
@Test
public void testGetContentOk() throws Exception {
ConnectionFactory factory = context.mock(ConnectionFactory.class);
InputStream mockStream = context.mock(InputStream.class);
context.checking(new Expectations(){
{
oneOf(factory).getData();
will(returnValue(mockStream));
atLeast(1).of(mockStream).read();
will(onConsecutiveCalls(
returnValue(Integer.valueOf((byte)'W')),
returnValue(Integer.valueOf((byte)'o')),
returnValue(Integer.valueOf((byte)'r')),
returnValue(Integer.valueOf((byte)'k')),
returnValue(Integer.valueOf((byte)'s')),
returnValue(Integer.valueOf((byte)'!')), returnValue(-1)));
oneOf(mockStream).close();
}
});
WebClient2 client = new WebClient2();
String workingContent = client.getContent(factory);
assertEquals("Works!", workingContent);
}
@Test
public void testGetContentCannotCloseInputStream() throws Exception {
ConnectionFactory factory = context.mock(ConnectionFactory.class);
InputStream mockStream = context.mock(InputStream.class);
context.checking(new Expectations(){
{
oneOf(factory).getData();
will(returnValue(mockStream));
oneOf(mockStream).read();
will(returnValue(-1));
oneOf(mockStream).close();
will(throwException(new IOException("cannot close")));
}
});
WebClient2 client = new WebClient2();
String workingContent = client.getContent(factory);
assertNull(workingContent);
}
}
JMock도 마찬가지로 두 가지 테스트를 작성했다.
JMock은 EasyMock과 비교하면 JUnit5와 더 잘 통합되어 있고, 프로그래밍 방식으로 context 필드를 정의할 수 있다.
Mockito
Maven
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.13.0</version>
</dependency>
Gradle
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: '5.13.0'
Mockito를 활용한 TestAccountService
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
// JUnit5 확장
@ExtendWith(MockitoExtension.class)
class TestAccountServiceMockito {
@Mock
private AccountManager mockAccountManager;
@Test
public void testTransferOk() {
Account senderAccount = new Account("1", 200L);
Account beneficiaryAccount = new Account("2", 100L);
Mockito.lenient()
.when(mockAccountManager.findAccountForUser("1"))
.thenReturn(senderAccount);
Mockito.lenient()
.when(mockAccountManager.findAccountForUser("2"))
.thenReturn(beneficiaryAccount);
AccountService accountService = new AccountService();
accountService.setAccountManager(mockAccountManager);
accountService.transfer("1","2", 50L);
assertEquals(150L, senderAccount.getBalance());
assertEquals(150L, beneficiaryAccount.getBalance());
}
}
@ExtendWith를 통해 테스트를 확장한다. MockitoExtension 클래스는 @Mock 모의 객체를 만드는데 필요하다.
// JUnit5 확장
@ExtendWith(MockitoExtension.class)
when 메서드를 사용하여 모의 객체가 수행할 동작을 기대한다.
추가적으로 테스트에서 모의객체 메서드를 엄격하게 호출하지 못하도록 lenient 메서드를 사용한다.
lenient 메서드가 없으면 동일한 findAccountForUser 메서드에 대해 기대를 하나밖에 선언할 수 없다.
Mockito.lenient()
.when(mockAccountManager.findAccountForUser("1"))
.thenReturn(senderAccount);
Mockito.lenient()
.when(mockAccountManager.findAccountForUser("2"))
.thenReturn(beneficiaryAccount);
Mockito를 활용한 TestWebClient
import org.junit.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TestWebClientMockito {
@Mock
private ConnectionFactory factory;
@Mock
private InputStream mockStream;
@Test
public void testGetContentOk() throws Exception {
when(factory.getData()).thenReturn(mockStream);
when(mockStream.read()).thenReturn((int) 'W')
.thenReturn((int) 'o')
.thenReturn((int) 'r')
.thenReturn((int) 'k')
.thenReturn((int) 's')
.thenReturn((int) '!')
.thenReturn(-1);
WebClient2 client = new WebClient2();
String workingContent = client.getContent(factory);
assertEquals("Works!", workingContent);
}
@Test
public void testGetContentCannotCloseInputStream() throws Exception {
when(factory.getData()).thenReturn(mockStream);
when(mockStream.read()).thenReturn(-1);
doThrow(new IOException("cannot close"))
.when(mockStream).close();
WebClient2 client = new WebClient2();
String workingContent = client.getContent(factory);
assertNull(workingContent);
}
}
Mockito는 프로그래밍 방식으로 모의 객체를 만든 JMock과 달리 JUnit5 @ExtendWith나 @Mock을 사용하여 JUnit5 확장 모델과 통합해 사용할 수 있다. 따라서 다른 프레임워크보다 더 많이 쓰인다.