DEV/JUnit

[JUnit] 모의 객체 프레임워크 EasyMock / JMock / Mockito

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