-
[JUnit] 모의 객체 프레임워크 EasyMock / JMock / MockitoDEV/JUnit 2024. 9. 11. 00:06
모의 객체를 사용하기 위한 유용한 클래스를 제공하는 오픈 소스 프레임워크
- 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 확장 모델과 통합해 사용할 수 있다. 따라서 다른 프레임워크보다 더 많이 쓰인다.
'DEV > JUnit' 카테고리의 다른 글
[JUnit] JUnit5 extension (확장 모델) (1) 2024.10.22 [JUnit] 모의객체를 활용해 리팩터링하기 (0) 2024.08.27 [JUnit] 모의 객체로 테스트하기 (0) 2024.08.25 [JUnit] 스텁을 활용한 테스트 (0) 2024.07.14 [JUnit] JUnit4에서 JUnit5로 마이그레이션 (0) 2024.06.30