ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JUnit] 모의 객체 프레임워크 EasyMock / JMock / Mockito
    DEV/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인 expectandReturn 메서드를 사용한다.

    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 확장 모델과 통합해 사용할 수 있다. 따라서 다른 프레임워크보다 더 많이 쓰인다.

    댓글

Designed by Tistory.