본문 바로가기
Spring/테스트

[테스트 주도 개발 시작하기] 부록C. Mockito 기초 사용법

by compile-this 2024. 9. 9.

 

 

Mockito 프레임워크

모의 객체 생성, 검증, stub을 지원하는 프레임워크

 

의존 설정

JUnit과 Mockito는 서로 독립적이기 때문에, 버전 간 의존성은 없기 때문에 최신에 안정된 버전을 선택하면 된다.

https://mvnrepository.com/artifact/org.mockito/mockito-core

 

모의 객체 생성

Mockito.mock() 메서드를 이용하면 특정 타입의 모의 객체를 생성할 수 있다.

 

[Mockito.mock() 메서드를 이용한 모의 객체 생성]

public class GameGenMockTest {
    @Test
    void mockTest() {
        GameNumGen genMock = mock(GameNumGen.class);
    }
}

public interface GameNumGen {
    String generate(GameLevel level);
}
  • Mockito.mock() 메서드는 클래스, 인터페이스, 추상 클래스에 대해 모의 객체를 생성할 수 있다.

 

스텁 설정

모의 객체를 생성한 뒤, BDDMockito 클래스를 이용해서 모의 객체에 stub을 구성할 수 있다.

  • stub: 구현을 단순한 것으로 대체

 

[BDDMockito.given()을 이용한 stub 설정]

public class GameGenMockTest {
    @Test
    void mockTest() {
        GameNumGen genMock = mock(GameNumGen.class);
        given(genMock.generate(GameLevel.EASY)).willReturn("123");

        String genNum = genMock.generate(GameLevel.EASY);
        assertEquals("123", genNum);
    }
}
  • BDDMockito클래스의 given() 메서드는 stub을 정의할 mock객체의 메서드 호출을 전달
  • willReturn() 메서드는 stub을 정의한 메서드가 리턴할 값을 지정

 

[특정 타입의 exception을 발생하도록 stub 설정]

@Test
void mockThrowTest() {
    GameNumGen genMock = mock(GameNumGen.class);
    given(genMock.generate(null)).willThrow(IllegalArgumentException.class);

    assertThrows(IllegalArgumentException.class, () -> {
        genMock.generate(null);
    });
}
  • willThrow() 메서드를 이용하여 stub을 정의한 메서드가 exception을 발생하게 설정

 

[리턴 타입이 void인 메서드에 대해 exception을 발생하도록 stub 설정]

public class VoidMethodStubTest {
    @Test
    void voidMethodWillThrowTest() {
        List<String> mockList = mock(List.class);
        willThrow(UnsupportedOperationException.class)
                .given(mockList)
                .clear();

        assertThrows(UnsupportedOperationException.class, mockList::clear);
    }
}
  • BDDMockito.willThrow() 메서드는 발생할 exception 타입이나 객체를 인자로 받고,
    이어서 given() 메서드에서는 mock 객체의 메서드 실행이 아닌, mock 객체를 전달받는다.
  • given() 메서드는 mock 객체 자신을 리턴하고, 이때 exception을 발생하는 메서드( clear() )를 호출한다.
    • clear()를 호출한다고 실제로 호출되지 않고, exception을 발생할 mock 객체의 메서드를 설정한다.

 

인자 매칭 처리

org.mockito.ArgumentMatchers 클래스를 통해 stub으로 정의된 메서드의 인자 값을
정확하게 일치하는 값 대신 임의의 값에 일치하도록 설정할 수 있다.

 

[ArgumentMatchers.any() 메서드로 인자 값 매칭 처리]

public class AnyMatcherTest {
    @Test
    void anyMatchTest() {
        GameNumGen genMock = mock(GameNumGen.class);
        given(genMock.generate(any())).willReturn("456");

        String genNum = genMock.generate(GameLevel.EASY);
        assertEquals("456", genNum);

        String genNum2 = genMock.generate(GameLevel.NORMAL);
        assertEquals("456", genNum2);
    }
}
  • ArgumentMatchers.any() 메서드를 인자 위치에 전달하여 메서드의 인자에 모든 값이 일치하도록 stub을 설정한다.
    • generate() 메서드에 어떤 인자가 오든 "456"을 리턴한다.

 

[stub을 설정한 메서드의 인자가 두개 이상인 경우]

@Test
void invalidUseAnyMatch_Then_ThrowException() {
    List<String> mockList = mock(List.class);
    given(mockList.set(anyInt(), "123")).willReturn("456");
}
  • mockList.set() 메서드의 stub을 설정할 때 첫번째 인자는 ArgumentMatchers.anyInt() 메서드를 이용하고,
    두번째 인자는 "123"이라는 특정 값에 일치하도록 설정했다.

 

위 테스트 코드를 실행하면 아래와 같은 에러가 발생한다.

 

위 exception은 org.mockito.internal.invocation.MatcherBinders의 validateMatchers()에서 발생한다.

  • ArgumentMatchers의 메서드는 내부적으로 ArgumentMatcher로 등록된다.
  • Mockito는 한 인자라도 ArgumentMatcher를 사용해서 설정한 경우 위 코드 처럼 모든 인자를
    ArgumentMatcher를 이용해서 설정하도록 한다.

 

위에 exception이 발생한 코드를 올바르게 동작하도록 수정해보면 아래와 같다.

[임의 값 일치와 정확한 값 일치가 필요한 경우 eq() 메서드를 사용]

@Test
void mixAnyAndEq() {
    List<String> mockList = mock(List.class);
    given(mockList.set(anyInt(), eq("123"))).willReturn("456");

    String old = mockList.set(5, "123");
    assertEquals("456", old);
}

 

 

 

행위 검증

모의 객체의 역할 중 하나는 실제로 모의 객체가 호출되었는지 검증하는 것이다.

 

[모의 객체의 메서드의 호출 여부 검증 코드]

public class GameTest {
    @Test
    void init() {
        GameNumGen genMock = mock(GameNumGen.class);
        Game game = new Game(genMock);
        game.init(GameLevel.EASY); //init()에서 GameNumGen의 generate()를 호출한다고 가정

        then(genMock).should().generate(GameLevel.EASY);
    }
}
  • BDDMockito.then() - 메서드 호출 여부를 검증할 모의 객체를 전달받는다.
  • should() - 모의 객체의 메서드가 호출되어야 한다는 것을 설정한다.
  • should()메서드 이후 - 실제로 호출될 메서드( generate() )를 지정한다.

[모의 객체의 메서드가 정확히 한 번만 호출되었는지 검증하는 코드]

@Test
void initCalledOnlyOnce() {
    GameNumGen genMock = mock(GameNumGen.class);
    Game game = new Game(genMock);
    game.init(GameLevel.EASY); //init()에서 GameNumGen의 generate()를 호출한다고 가정

    then(genMock).should(only()).generate(GameLevel.EASY);
}
  • should() 함수에 Mockito.only()를 넘겨주면 설정한 모의 객체의 메서드가 한번만 호출되었는지 검증할 수 있다.

 

인자 캡쳐

모의 객체의 메서드를 호출할 때 사용한 인자를 검증할 때 사용한다.

 

[ArgumentCaptor를 이용한 모의 객체 메서드 호출 인자 사용]

public class ArgumentCaptorTest {
    private UserRegister userRegister;
    private EmailNotifier mockEmailNotifier = mock(EmailNotifier.class);
    
    @Test
    void whenRegisterThenSendMail() {
        // register()에서 emailNotifier의 sendRegisterEmail() 호출한다고 가정
        userRegister.register("id", "pw", "email@email.com");  

        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
        then(mockEmailNotifier)
                .should().sendRegisterEmail(captor.capture());
        
        String realEmail = captor.getValue();
        assertEquals("email@email.com", realEmail);
    }
}
  • ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
    • String 타입의 인자를 보관할 수 있는 ArgumentCaptor를 생성한다.
  • should().sendRegisterEmail(captor.capture());
    • 모의 객체의 메서드 호출 여부를 검증하는 코드에서 인자로 위에서 생성한 ArgumentCaptor#captor() 메서드를 전달한다.
  • captor.getValue();
    • 모의 객체의 메서드를 실행할 때 사용한 인자는 ArgumentCaptor#getValue() 메서드로 구할 수 있다.

 

JUnit5 확장 설정

Mockito의 JUnit5 의 확장 기능을 사용하면 Mockito.mock()을 이용해서 모의 객체를 생성하는 대신,
Annotation을 통해서 간편하게 생성할 수 있다.

 

[Mockito JUnit Jupiter 의존성 추가]

testImplementation 'org.mockito:mockito-junit-jupiter:5.9.0'
// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter

 

[JUnit5 확장 기능을 통한 @Mock 애노테이션으로 모의 객체 생성]

@ExtendWith(MockitoExtension.class)
public class JUnit5ExtensionTest {

    @Mock
    private GameNumGen gameGen;

    @Test
    void mockTest() {
        assertNotNull(gameGen);
    }
}