안녕하세요!
모두 현업에서든, 개인 프로젝트에서든 테스트코드 열심히 작성하고 계신가요?
테스트코드 작성이 번거롭다고 느낄 수 있지만, 개인적으로는 꼭 필요한 단계라고 생각하는데요.
그래서!
자바와 스프링을 사용하며 테스트 코드를 작성하다보면, 무조건 들어보게 되는 Mockito 프레임 워크에 대해 정리하고, 실제 어떻게 적용하는지 알아보려고 합니다.
그럼 시작해보도록 하겠습니다!
스프링과 Junit을 이용해서 테스트 코드를 작성하다 보면 테스트 환경(DB, API)을 구현하는 코드까지 작성해야 할 때가 있습니다.
하지만 막상 작성하게 되면 실제 테스트할 코드보다 환경을 구현하는 코드가 훨씬 복잡해지게 됩니다.
이런 이슈를 해결하기 위해서 Test Double 이라는 것이 나왔고, Java진영에서는 대표적으로 Mockito가 있습니다.
Mockito 테스트 프레임워크는 Mock 이라는 가짜 객체를 생성하여, 테스트를 가능하도록 해주는 프레임워크입니다.
따라서 아직 구현체가 없어도 테스트가 가능하기 때문에 일반적으로 단위 테스트에 많이 쓰입니다.
Mock 개념에 대해 더 자세히 알아보도록 하겠습니다.
Mock 은 ‘모의, 가짜의’ 라는 뜻을 가집니다.
즉, 테스트 시 실제 객체와 동일한 ‘모의, 가짜의’ 객체를 생성하여 테스트의 효용성을 높여줍니다.
모의 객체(Mock Object)란 주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트할 경우 테스트를 수행할 모듈과 연결되는 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는 데 사용하는 객체이다. 사용자 인터페이스(UI)나 데이터베이스 테스트 등과 같이 자동화된 테스트를 수행하기 어려운 때 널리 사용된다 - 위키백과 Mock 정의
예를들어 보겠습니다.
DB에서 특정 테이블을 읽어 객체를 리턴하는 메소드를 테스트하는 경우를 가정해보도록 하겠습니다.
해당 메소드를 Mock 객체 없이 테스트 한다면, 매번 DB를 읽어오는, 부하가 많이 걸리는 작업을 수행하게 됩니다.
이때, Mock 객체(가짜 객체)를 사용하여 테스트한다면,
직접 DB에 접근하지 않고 테스트 시간도 줄이면서 불필요한 리소스 소비를 막고, 객체의 행동까지 개발자 마음대로 조정이 가능합니다.
Mock을 간편하게 생성하고, Mock의 행동을 정해주는 stubbing, 정상적으로 작동하는지에 대한 verify 등 Mock에 대해 다양한 기능을 제공해주는 오픈소스 프레임워크입니다.
Mockito 는 Mock 과 Mojito의 합성어입니다
Mockito 공식문서
Mockito의 기초가 되는 기능들을 하나씩 알아보도록 하겠습니다.
직접 테스트코드를 만들어보셔도 되고,
아래 예시들을 가볍게 읽어보셔도 충분히 이해 되실겁니다.
Stub은 토막, 남은 부분이라는 뜻을 가지고 있습니다.
정의와 어떤 관계가 있는지 모르겠지만, 테스트 중에 만들어진 호출에 대해 미리 준비된 답변을 제공하는 것인데요
즉, Mock 객체가 어떤 메소드를 실행 한 경우, 사용자가 원하는 리턴 값을 정의해줄 수 있습니다.
큰 틀의 사용법은 when 매소드를 통해 이루어지지만, 상세한 Stubbing 방법에는 두가지가 있습니다.
OngoingStubbing
OngoingStubbing는 일반적으로 사용하는 stubbing 입니다.
when({stubbing 할 메소드}}.{OngoingStubbing 메소드};
위 코드처럼 when에 넣은 메소드의 리턴 값을 정의해주는 메소드이죠.
메소드 종류는 아래와 같습니다.
메소드명 | 설명 |
---|---|
thenReturn | 스터빙한 메소드 호출 후 어떤 객체를 반환할 건지 정의 |
thenThrow | 스터빙한 메소드 호출 후 어떤 예외를 반환할 건지 정의 |
thenAnswer | 스터빙한 메소드 호출 후 어떤 작업을 할지 커스텀하게 정의(mockito javadoc을 보면 이 메소드를 굳이 사용하지 말고 thenReturn, thenThrow 메소드 사용을 추천하고 있습니다) |
thenCallRealMethod | 실제 메소드 호출 |
이론 설명은 이쯤하고, 실제 예시로 알아볼까요?
예시를 위해 Game 클래스와 GameService를 생성해줍니다.
관련 메소드 하나씩 테스트 해보겠습니다.
(@Mock 어노테이션을 통해 GameService의 가짜 객체를 생성합니다)
결과는 모두 정상 통과입니다.
Stubber
{Stubber 메소드}.when({stubbing 할 클래스}).{stubbing 할 메소드};
위와 같이 Stubber 는 OngoingStubbing과 다르게 when에 클래스를 넣고, 그 후에 메소드를 호출합니다.
순서만 바뀐 것 같지만 큰 차이가 있습니다.
Stubber 의 경우는 원하는 리턴 값을 "먼저" 설정해줘야하는 경우에 사용합니다.
또한 Stubber 메소드 사용 시 리턴 값이 void 인 메소드 테스트가 가능합니다.
메소드 종류는 아래와 같습니다.
메소드명 | 설명 |
---|---|
doReturn | 스터빙 메소드 호출 후 어떤 행동을 할 건지 정의 |
doThrow | 스터빙 메소드 호출 후 어떤 Exception을 반환할 건지 정의 |
doAnswer | 스터빙 메소드 호출 후 작업을 할지 커스텀하여 정의 |
doNothing | 스터빙 메소드 호출 후 어떤 행동도 하지 않게 정의 |
doCallRealMethod | 실제 메소드 호출 |
예시를 들어 설명해볼까요?
상황에 맞게 적절한 스터빙 방법을 사용하면 될 것 같습니다.결과는 모두 정상 통과입니다
체이닝 Stubbing (참고)
여러개의 스터빙 메소드를 설정하여 연속적으로 리턴 값을 지정해줄 수 있습니다.
스터빙은 아직 구현체가 없는 인터페이스 메소드 혹은 단위테스트 시 유용하게 사용할 수 있는 것 같습니다!
다음 기능으로 넘어가 보겠습니다.
Verify 메소드는 2-1에서 수행한 스터빙 메소드가 정상적으로 수행되었는지 검증해볼 수 있는 기능입니다.
정확히는 Mock 객체의 호출여부와 호출 횟수를 검증하는 기능이죠. 값 자체의 검증에는 관여하지 않습니다.
verify(T mock, VerificationMode mode)
위와 같은 형태로 쓰이며, VerificationMode는 검증할 값을 정의하는 메소드입니다.
메소드 종류는 다음과 같습니다.
메소드명 | 설명 (테스트 내에서) |
---|---|
times(n) | 몇 번 호출됐는지 검증 |
never | 한 번도 호출되지 않았는지 검증 |
atLeastOne | 최소 한 번은 호출됐는지 검증 |
atLeast(n) | 최소 n 번이 호출됐는지 검증 |
atMostOnce | 최대 한 번이 호출됐는지 검증 |
atMost(n) | 최대 n 번이 호출됐는지 검증 |
calls(n) | n번이 호출됐는지 검증 (InOrder랑 같이 사용해야 함) |
only | 해당 검증 메소드만 실행됐는지 검증 |
timeout(long mills) | n ms 이상 걸리면 Fail 그리고 바로 검증 종료 |
after(long mills) | n ms 이상 걸리는지 확인timeout과 다르게 시간이 지나도 바로 검증 종료가 되지 않는다. |
description | 실패한 경우 나올 문구 |
마찬가지로 예를 들어 보겠습니다!
v1에서는 스터빙하지 않은 Mock 객체의 호출 횟수만을 검증하였고,
v2에서는 스터빙 메소드를 적용해본 후 정상적으로 호출 되었는지 검증하였습니다.
결과는 모두 정상 작동입니다.
하나씩 차근차근 보시면 어렵지 않게 이해되실 겁니다.
calls 메소드는 In order 를 소개한 후 적용시켜 보겠습니다.
Argument Matcher는 메소드 호출 시 파라미터로 전달된 값을 검증하거나, 스터빙하기 위해 사용되는 기능입니다.
대표적으로 anyInt(), anyString() 등이 존재합니다.
이러한 Argument Matcher는 모든 값 또는 특정 조건을 충족하는 값과 일치하는 파라미터를 검증합니다.
즉, 메소드 호출 시 전달되는 파라미터를 검증하는 것!입니다.
바로 예시를 들어보겠습니다.
예시를 보니 이해가 잘 되시나요?
결과는 잘 통과되었습니다.
이제 마지막 기능으로 넘어가 볼까요?
InOrder는 Mock 객체의 호출 순서를 검증할 수 있는 기능입니다.
Mock 객체를 여러번 호출 시 원하는 순서대로 호출되었는지 검증하는 것이 목적입니다.
2-2 에서 남겨두었던 calls() 메소드와 함께 예시로 알아보겠습니다.
먼저 InOrder만 사용하는 경우를 보겠습니다.
이렇게 주요 기능 4가지에 대해 알아봤습니다.
위에 주요 기능들을 공부하며 어느정도 예상 하셨겠지만 가짜 객체, 즉 Mock 객체를 생성하는 방법은 여러가지가 있습니다.
간단하게 하나씩 알아보도록 하겠습니다.
@Mock 어노테이션을 사용하면 간편하게 Mock 객체를 생성할 수 있습니다.
주의할 점은 기본 생성자가 존재해야만 Mock 객체를 만들어 사용할 수 있습니다.
또한, @Mock으로 만든 Mock 객체는 메소드를 호출해서 사용하려면 반드시 스터빙을 해야합니다.
스터빙을 하지 않고 그냥 호출한다면, primitive type은 0, 참조형은 null을 반환하게 됩니다.
@Mock
Game game;
@Spy 어노테이션으로도 Mock 객체를 생성할 수 있습니다.
이렇게 만든 Mock 객체는 실제 객체를 복사한 객체로 메소드 실행 시 스터빙을 하지 않으면 기존 객체의 로직을 실행한 값을, 스터빙을 한 경우엔 스터빙 값을 리턴합니다.
@Spy 는 여러 메소드 중 선택적으로 Stub 하고 싶은 경우 사용하는 경우가 많습니다.
어노테이션 이외에도 mock() 메소드로 Mock 객체를 생성할 수 있습니다.
@Mock과 마찬가지로 만들어진 Mock 객체를 스터빙 해주지 않으면, primitive type은 0, 참조형은 null을 반환하게 됩니다.
List<String> list = Mockito.mock(ArrayList.class);
@Spy와 마찬가지로 spy() 매소드를 통해 실제 객체의 인스턴스를 복사하여 똑같이 동작하는 Mock을 생성할 수 있습니다.
List<String> list = new ArrayList<>();
List<String> spyList = Mockito.spy(list);
이렇게 긴 호흡으로 Mockito 프레임워크에 대해서 자세히 알아봤습니다.
이제 뭔가 테스트 코드를 무조건 짜고 싶은 생각이 들지 않나요?
한 번 꼼꼼하게 공부하고, 정리해놓으면 유용하게 사용할 수 있을 것 같습니다.
읽어주셔서 감사합니다! 😊
References
Mockito - mockito-core 5.8.0 javadoc