Mockito-Kotlin Sample로 자세히 알아보기
Mockito-Kotlin Sample로 자세히 알아보기
#Mockito Features에 대한 문서를 글쓴이가 Kotlin으로 해석한 샘플입니다.
틀린 것이나 좀 더 나은 코드가 있다면 댓글로 남겨주시면 감사하겠습니다 :)
#Mockito Features 원문
https://www.javadoc.io/static/org.mockito/mockito-core/3.1.0/org/mockito/Mockito.html
contents
1. Mock객체의 동작(Behavior)을 검증해보기
@Test
fun `mock_will_memorize_all_interaction`() {
val mockedList = mock<MutableList<String>>()
mockedList.add("one")
mockedList.clear()
//검증하기 , mock은 모든 상호작용을 기억하고 있습니다.
//사용자는 mock의 어떤 메소드가 실행되었는지 선택적으로 검증 가능합니다.
verify(mockedList).add("one")
verify(mockedList).clear()
//검증 실패 케이 -add("two")가 호출되지않았으므로 실패
//verify(mockedList).add("two")
}
mock객체는 add() , clear() 같은 객체에 대한 상호작용에 대해서 기억하고 있습니다.
verify()를 이용하여 상호작용에 대하여 검증 가능합니다.
상호작용이 일어나지 않은 add("two")를 검증하면 테스트 실패합니다.
2. stubbing은 어떻게 하는 거지?
stub은 테스트에서 호출된 요청에 대해 미리 준비해둔 결과를 제공합니다.
@Test(expected = RuntimeException::class)
fun `how to stubbing`() {
// interface 뿐 아니라 구체 클래스도 mock으로 만들 수 있습니다.
val mockedList = mock<LinkedList<String>>()
//stubbing
whenever(mockedList[0]).thenReturn("first")
whenever(mockedList[1]).thenThrow(RuntimeException())
println(mockedList[0]) //첫 번째 element를 출력합니다.
println(mockedList[1]) //RuntimeException occurred
println(mockedList[999]) //999 element는 stub 되지않았으므로 null 출력합니다.
// stubbing 된 부분이 호출되는지 확인할 수 있긴 하지만 불필요한 일입니다.
// 만일 코드에서 get(0)의 리턴값을 확인하려고 하면, 다른 어딘가에서 테스트가 깨집니다.
// 만일 코드에서 get(0)의 리턴값에 대해 관심이 없다면, stubbing되지 않았어야 합니다.
verify(mockedList)[0]
}
Interface뿐만 아니라 구체 클래스(구현이 있는 클래스)도 mock으로 만들 수 있습니다.
mockedList.get(0)은 "first"를 리턴하도록 stub 하고
mockedList.get(1)은 RuntimeException을 얻도록 stub 합니다.
mocekdList.get(999)는 stub 되지 않았으므로 null을 얻습니다.
3. Argument Matchers
@Test
fun `argument_matcher`() {
val mockedStringList =
mock<LinkedList<String>>() //val mockedIntList:LinkedList<Int> = mock()
val mockedFloatList = mock<LinkedList<Float>> {
on { this[ArgumentMatchers.anyInt()] } doReturn 3f
}
//내장된 argument matcher인 anyInt()를 이용한 stubbing
// 모든 Integer 타입 매개변수를 받을 경우 "element"를 돌려줍니다.
whenever(mockedStringList[ArgumentMatchers.anyInt()]).thenReturn("elements")
println(mockedStringList[2])
println(mockedFloatList[3])
println(mockedFloatList[3])
}
LinkedList <String> mock 객체와 LinkedList <Float> mock객체를 만듭니다.
LinkedList <Float> 객체는 mock객체로 만들면서 바로 on { } doReturn을 이용하여 Stubbing 하여 줍니다.
on { this[ArgumentMatchers.anyInt()] } doReturn 3f
위의 코드는 this.get(anyInt())
즉 어떤 Int형 숫자를 넣든 간에
doReturn 3f
3f를 반환합니다.
아래의 whenever도 마 찬자기로 mockedStringList.get(anyInt())
어떤 Int형 숫자를 넣든 간에
thenReturn "elements"
elements라는 스트링을 반환합니다.
즉 , Argument Matcher를 이용하면 임의의 인자 값을 지정할 수 있습니다.
예를 들어, 임의의 정수 값을 인자로 전달받은 메서드 호출을
when()과 verify()에서 표현하고 싶다면 다음과 같이 Matchers.anyInt() 메서드를 사용하면 됩니다.
Matchers 클래스는 anyInt() 뿐만 아니라 anyString(), anyDouble(), anyLong(), anyList(), anyMap() 등의 메서드를 제공하는데, 이들 메서드에 대한 자세한 내용은 아래 공식 document를 보는 것을 추천드립니다.
https://javadoc.io/static/org.mockito/mockito-core/3.1.0/org/mockito/ArgumentMatchers.html
그리고 만약 여러 인자에 , agument matcher를 사용한다면
모든 인자를 agument matcher로 사용해야만 합니다.
세 개의 인자가를 stub으로 넣어주어야 할 때 2개는 anyXXX()로 임의의 값을
나머지 1개의 인자는 특정 값으로 명시하고 싶다면 아래와 같이 지정합니다.
@Test
fun `verify_eq`() {
val mock = mock<TestClass>()
whenever(
mock.someMethod(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
eq("3")
)
).thenReturn("firstArgument secondArgument 3")
println(mock.someMethod("1", "2", "3"))
//인자 중 한가지라도 Argument Matcher를 사용하면 나머지 인자에 대해서도 Matcher를 사용해야 한다.
//인자 중 특정한 값을 명시해야한다면 eq()를 사용한다
verify(mock).someMethod(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), eq("3"))
/** error
* verify(mock).someMethod(ArgumentMatchers.anyInt(),ArgumentMatchers.anyString(), "3")
* */
}
위의 코드처럼
두 개의 인자에 a mrguatcher를 사용하고 , 나머지 1개의 인자를 특정값으로 사용하고 싶다면 eq()를 사용하여 인자 값을 넣어줍니다.
주석의 내용을 보면 , eq()를 쓰지 않으면 에러가 발생합니다.
4. 몇 번 호출됐는지 / 최소한 한 번 호출됐는지 / 호출되지 않았는지 확인하기
verity으로 검증할 때 해당 내용이 몇 번 호출되었는지,
최소한 한번 호출되었는지 , 호출되지 않았는지도 검증할 수 있습니다.
verify의 2번째 인자(파라미터)로 넣어서 설정합니다.
- atLeastOnece() : 적어도 한번 수행했는지 검증
- atLeast(int n) : 적어도 n 번 수행했는지 검증
- times(int n) : 무조건 n번 수행했는지 검증 (n보다 크거나 작으면 오류로 간주)
- atMost(int n) : 최대한 n 번 수행했는지 검증
- never() : 수행되지 않았는지 검증(수행했으면 오류로 간주)
- timeout(long millisecond ) : 주어진 시간에 초과하였는지 검증(초과하였으면 오류로 간주)
@Test
fun `verifying exact_number of invocations, at least ,never, time`() {
val mockedList = mock<MutableList<String>>()
mockedList.add("once")
mockedList.add("twice")
mockedList.add("twice")
mockedList.add("three times")
mockedList.add("three times")
mockedList.add("three times")
//아래의 두 검증 방법은 동일하다
verify(mockedList).add("once")
verify(mockedList, times(1)).add("once")
//지정된 회수만큼 호출되었는지 검증
verify(mockedList, times(2)).add("twice")
verify(mockedList, times(3)).add("three times")
//never()를 이용한 검증. never() == times(0)
verify(mockedList, never()).add("never happaned")
mockedList.add("five times")
mockedList.add("five times")
mockedList.add("three times")
mockedList.add("three times")
/**error
* mockList.add("three times")
* */
verify(mockedList, atLeastOnce()).add("three times")
verify(mockedList, atLeast(2)).add("five times")
verify(mockedList, atMost(5)).add("three times")
//시간,횟수 검
verify(mockedList, timeout(100).times(5)).add("three times")
}
5. void method의 exception stubbing
void를 리턴 형식으로 갖는 메서드는 stub 하는 법이 약간 다릅니다.
위에서 설명한 일반 stubbing은 whenever(mock.method()). thenReturn(value)(mock.method()). thenReturn(value)형식인데,
mock.method()이 void값을 가지면 whenever(void)처럼 되기 때문에 문법에 맞지 않기 때문입니다.
void(kotlin에서는 Unit)을 그냥 stub 하게 되면 아래와 같은 에러 문구를 볼 수 있을 겁니다.
void method exception stubbing in kotlin
그래서 대신
대신 doThrow(new Exception()). whenever(mock). method()처럼
@Test(expected = RuntimeException::class)
fun `void method exception stubbing`() {
val mockedList = mock<MutableList<String>>()
doThrow(RuntimeException()).whenever(mockedList).clear()
mockedList.clear()
}
혹은 아래와 같이 doNothing으로 stub 하여 하용하는 방법도 존재합니다.
@Test
fun `void method exception stubbing in kotlin`() {
val mockedList = mock<MutableList<String>>()
doNothing().whenever(mockedList).clear()
mockedList.clear()
verify(mockedList, times(1)).clear()
}
6. 순서 검증하기
Mockito- kotlin에서는
inOrder lambda block을 이용하여 순서를 검증할 수 있습니다.
inline fun inOrder(
vararg mocks: Any,
evaluation: InOrder.() -> Unit
) {
Mockito.inOrder(*mocks).evaluation()
}
인라인 함수 inOrder를 호출하여 람다 안에 검증한고 싶은 순서대로 검증할 내용 verify()를 입력합니다
mocks는 vararg (가변 인자)로 선언되어있어 여러 개 mock객체를 인자로 받을 수 있습니다.
@Test
fun `verification in order`() {
val singleMock =
mock<MutableList<String>>()
.apply {
add("was added first")
add("was added second")
}
inOrder(singleMock) {
verify(singleMock).add("was added first")
verify(singleMock).add("was added second")
}
val firstMock = mock<MutableList<String>>()
val secondMock = mock<MutableList<String>>()
firstMock.add("was called first")
secondMock.add("was called second")
//A + B 두개의 mock을 mix 가능
inOrder(firstMock, secondMock) {
verify(firstMock).add("was called first")
verify(secondMock).add("was called second")
}
}
7. 아무 일도 일어나지 않은(어떤 상호작용도 없었던) mock에 대한 검증
verifyZeroInteractions함수를 사용하여 여러 개의 mock객체들이 상호작용이 있었는지 없었는지를 검증합니다.
fun verifyZeroInteractions(vararg mocks: Any) {
Mockito.verifyZeroInteractions(*mocks)
}
@Test
fun `making sure interactions never happened on mock`() {
val mockOne = mock<MutableList<String>>()
val mockTwo = mock<MutableList<String>>()
val mockThree = mock<MutableList<String>>()
mockOne.add("one")
verify(mockOne).add("one")
verify(mockOne, never()).add("two")
//나머지 2개의 mock은 아무 일이 발생하지 않았음(no interactions). 그에대한 검증
verifyZeroInteractions(mockTwo, mockThree)
}
8. 불필요하게 실행되는 코드 찾기
verifyNoMoreInteractions() 함수를 사용하여 더 이상 호출이 있는지 없는지를 검증합니다.
fun <T> verifyNoMoreInteractions(vararg mocks: T) {
Mockito.verifyNoMoreInteractions(*mocks)
}
@Test(expected = NoInteractionsWanted::class)
fun `finding redundant invocations`() {
val mockedList = mock<MutableList<String>>()
mockedList.add("one")
mockedList.add("two")
verify(mockedList).add("one")
//아래 구문은 실패한다.
verifyNoMoreInteractions(mockedList)
}
위의 예를 보면
mocekdList에 "one", "two"를 add 합니다.
verify(mockedList). add("one")으로 add("one")이 호출되었는지 검증하고
그다음 verifyNomoreInteractions()를 호출하여 더 이상의 상호작용이 있었는지 체크합니다
위의 예에서는 add("two")라는 상호작용이 있었으므로 verifyNomoreInteractions()는 실패합니다.
9. 간단하게 mock 생성하기 - @Mock 어노테이션
1.8.3 버전부터 종종 유용하게 사용할 수 있는 annotation이 추가되었습니다.
- @Captor는 ArgumentCaptor 생성을 간략화시켰습니다. – 잡아야 하는 파라미터가 generic 클래스이고, 컴파일러 에러를 피하고 싶을 때 유용
- @Spy - spy(Object) 대신에 사용할 수 있습니다.
- @InjectMocks - 테스트될 객체에 mock을 자동으로 넣어줍니다.
- @Mock,@Spy 이 붙은 목객체를 자신의 멤버 클래스와 일치하면 주입시킨다.
- 쉽게 말해 실제 테스트할 클래스가 @injectMocks어노테이션을 사용해 목객체를 생성한다.
annotation들은 오직 MockitoAnnotations.initMocks(Object)으로 초기화해야 사용 가능합니다.
Anotaion의 장점
- 반복적인 mock 생성 코드를 줄여줍니다.
- 테스트 클래스의 가독성을 높여줍니다.
- 필드 이름으로 각각의 mock을 구분하기 때문에, 검증 시에 발생하는 에러를 좀 더 읽기 쉽게 만들어줍니다
class MockitoAnotaionSample {
@Captor
lateinit var captor: ArgumentCaptor<Foo>
@Spy
val spyOnFoo = Foo("argument")
@Spy
lateinit var spyOnBar: Bar
@InjectMocks
lateinit var manager: ArticleManager
@Before
fun init() {
MockitoAnnotations.initMocks(this)
}
}
중요! 아래 내용을 반드시 작성해주어야 동작합니다
MockitoAnnotations.initMocks(testClass);
내장 runner인 MockitoJUnitRunner를 이용하셔도 됩니다.
MockitoAnnotations에 대해 더 많은 정보를 읽고 싶으면 아래를 읽어보시길 바랍니다.
https://static.javadoc.io/org.mockito/mockito-core/3.1.0/org/mockito/junit/MockitoJUnitRunner.html
참고 - https://medium.com/@hanru.yeh/mockito-annotations-with-kotlin-82a3619496f2
10. 연속적인 stubbing 하기
stubbing을 할 때 해당 내용이 호출이 여러 번 되는 상황에서
1번째 호출과, 2번째 호출 내용, 3번째 호출 내용...... , N번째 호출 내용을 모두 다르게 설정할 수 있습니다.
@Test(expected = RuntimeException::class)
fun `stubbing consecutive calls`() {
val mock = mock<TestClass>()
whenever(mock.someMethod("some arg"))
.thenThrow(RuntimeException()) //first return
.thenReturn("foo") //second return
//first call occurred error
mock.someMethod("some arg")
//second call print foo
Assert.assertEquals("foo", mock.someMethod("some arg"))
//All subsequent calls print foo
Assert.assertEquals("foo", mock.someMethod("some arg"))
}
위의 코드를 보면 someMethod() 첫 번째 호출에는 "some arg"를
두 번째 someMethod() 호출에는 RuntimeException을
세 번째 someMethod() 호출에는 "foo"를 반환합니다.
11. callback을 stubbing 하기
이때까지의 예제에서는 stub 할 때 모두 특정값을 넣었습니다.
만약 mock의 상태나 메서드 인자 값에 따라 다른 값을 돌려주게 하게 만들고 싶다면 어떻게 해야 할까요?
Answer <?> 클래스를 사용하면 가능합니다.
하지만 아주 특별한 상황이 아니라면 크게 사용할 일은 없을 듯합니다.
@Test
fun `stubbing with callbacks`() {
val mock = mock<TestClass> {
on {
mock.someMethod(ArgumentMatchers.anyString())
} doAnswer {
if (it.arguments[0] == "foo")
"foo"
else
"none"
}
}
println(mock.someMethod("foo"))
println(mock.someMethod("ss"))
verify(mock).someMethod("foo")
verify(mock).someMethod("ss")
}
위 예제는 doAnser 블록 안에서 argument값을 확인하고
argument값에 따라 반환 내용을 다르게 합니다.
12. void method를 stubbing 하기 위한 doThrow || doAnswer || doNothing || doReturn
13. 실제 객체 감시하기
Mockito에서는 real 객체에 대한 스파이를 생성할 수 있습니다.
만약 spy(스파이)를 호출하게 되면 실제 method가 호출됩니다.( method가 stub 되지 않았을 경우)
real객체를 감시하는 것은 "Partial mocking"이라는 개념과 관련되어 있습니다.
partial mocking이란 클래스의 일부 method는 mocking 하고 그 나머지 method는 mocking 하고 싶을 때
즉, 일부만 mocking 하고자 할 때 사용합니다.
아래의 예로 사용방법을 보도록 하겠습니다.
@Test
fun `spying on real object`() {
val list = LinkedList<String>()
val spy = spy(list)
//특정 메소드만 stub하는것이 가능하다
whenever(spy.size).thenReturn(100)
//stub되지 않았기때문에 real method를 실행한다.
spy.add("one")
spy.add("two")
println(spy[0])
//stub된 size 100을 출력
println(spy.size)
verify(spy).add("one")
verify(spy).add("two")
verify(spy)[0]
verify(spy).size
}
@Test
fun `caution use spy`() {
val list = LinkedList<String>()
val spy = spy(list)
//실제 객체에 아무것도 없기 때문에 whenever호출시 IndexOutOfBoundsExcepsion이 발생
//whenever(spy[0]).thenReturn("foo")
//그렇기 때문에 doReturn으로 stubbing
doReturn("foo").whenever(spy)[0]
println(spy[0])
verify(spy)[0]
}
스파이(real object를 감시하기)를 할 때 알아야 할 중요한 점은
1. 가끔 stubbing 된 스파이에 대해 whenever(object)를 사용할 수 없습니다. (doReturn 등을 사용해야 합니다.)
2. final method는 조심해야 합니다. Mockito는 final method를 mock으로 만들지 않기 때문에 스파이(real object를 감시)하면서 final method를 stub 하게 되면 문제가 발생합니다. 실제 객체가 아닌 spy에 넘겨준 mock의 method가 호출됩니다. mock 객체는 필드를 초기화하지 않았기 때문에 일반적으로 NPE 가 발생합니다.
14. stubbing 되지 않은 method에 default값 설정하기
val mock = mock<TestSample>(defaultAnswer = Mockito.RETURNS_SMART_NULLS)
val mockTwo = mock<TestSample>(defaultAnswer = YourOwnAnswer())
mock 객체를 만들면서 생성자로 여러 인자를 넣어줄 수 있는데
그중에 default(기본적인 리턴 값)을 stub 되지 않은 메서드가 호출될 때 사용할 수 있습니다.
15. 파라미터 검증하기
arugmentCaptor클래스를 시용하여 파라미터로 들어오는 값들을 캡처하고
그 값들을 검증할 수 있습니다.
@Test
fun `capturing arguments for further assertions`() {
val myClass = mock<TestClass>()
myClass.someMethod("1", "2")
myClass.someMethod("3", "4")
argumentCaptor<String>().apply {
verify(myClass, times(2)).someMethod(capture())
Assert.assertEquals(4, allValues.size)
Assert.assertEquals("1", firstValue)
}
}
주의할 점은 ArgumentCaptor를 검증용으로만 사용해야 하고 stubbing용으로 사용하면 안 된다는 점입니다.
ArgumentCaptor는 블록 바깥쪽에서 만들어지기 때문에 stubbing 할 때 ArgumentCaptor를 사용하면 테스트 가독성이 떨어지게 됩니다.
또한 , stubbing 된 메서드가 호출되지 않으면 아무 파라미터도 잡히지 않기 때문에 결함의 범위가 넓어집니다.
16. Real partial mock
위(13번)에서 본 real Object 감시하기에서 본 내용의 연장선입니다.
spy을 이용하여 partial mock을 생성할 수 있습니다.
@Test
fun `real partial mocks`() {
// spy() method를 이용해 partial mock을 생성할 수 있다.
val list = spy(LinkedList<String>())
// mock에 대해 선택적으로 partial mock 기능이 동작하게 만들 수 있다.
val mock = mock<TestClass>()
// 실제 구현이 안전하다는 것을 확신할 수 있다.
// 만일 실제 구현이 예외를 던지거나 객체의 특정 상태에 의존하고 있다면 문제가 생길 것이다.
whenever(mock.someMethod()).thenCallRealMethod()
}
17. mock 다시 설정하기
보통은 mock을 리셋해서 사용하지 않고 각각의 테스트를 위해 새로운 mock을 생성합니다.
reset()을 사용하기보다는 지나치게 길거나 지나치게 자세한 테스트를 고쳐서 간결하고 한 가지만 테스트하는 테스트로 바꾸시길 바랍니다. code smell일 가능성이 제일 높은 경우는 테스트 method 중간에서 reset()을 사용한 경우입니다.
웬만하면 reset 사용은 지양하고 , 꼭 필요한 경우에만 reset()을 사용하도록 합시다.
@Test
fun `resetting mocks`() {
val mock = mock<MutableList<String>>()
whenever(mock.size).thenReturn(10)
Assert.assertEquals(10, mock.size)
reset(mock)
Assert.assertEquals(0, mock.size)
}
18. Aliases for behavior driven development
Behavior Driven Development 스타일의 테스트 작성방법은
테스트 method에 기본으로 //given //when //then이라고 주석을 달아두는 것입니다.
이렇게 함으로써 우리가 어떻게 테스트를 작성해야 하고, 여러분이 이렇게 만들도록 장려할 수 있습니다.
여기서 문제는 mockito에서 when을 이용한 구문들이 있기 때문에
bdd의 //given //when //then주석에 맞아떨어지지 않기 때문입니다.
왜냐하면 stubbing이 when이 아닌 given에 속하기 때문입니다.
Mockito는 그래서 BDD스타일의 테스트를 작성할 수 있도록 아래와 같은 메서드를 지원합니다.
@Test
fun `aliases for behavior driven development`() {
val calculatorService = mock<CalculatorService>()
//given
given(calculatorService.add(20.0, 10.0)).willReturn(30.0)
//when
val result = calculatorService.add(20.0, 10.0)
//then
Assert.assertThat(30.0, CoreMatchers.`is`(result))
}
verify 대신 `then`키워드를 사용하여 BDD 스타일로 검증할 수도 있습니다.
given(dog.bark()).willReturn(2);
// when
...
then(person).should(times(2)).ride(bike);
19. Mocking Details
MockingDetails클래스를 사용하여 Mock 된 객체의 정보를 알 수 있습니다.
- isMock() : 인자로 받은 객체가 Mock객체인지 리턴합니다.
- isSpy() : 인자로 받은 객체가 Spy로 생성된 객체인지 확인하여 리턴합니다.
- typeToMock : Mock객체의 ClassType을 리턴합니다.
- defaultAnswer: 기본으로 설정되어있는 defaultAnswer을 리턴합니다.
- invocations : mock객체에 일어났던 상호작용들을 List로 반환합니다.
- stubbing : mock객체에 Stubbing 된 내용들을 List로 반환합니다.
@Test
fun `mocks detail`() {
val mockedList = mock<List<String>> {
on { get(0) } doReturn "hello"
}
println("isMock : " + Mockito.mockingDetails(mockedList).isMock)
println("isSpy : " + Mockito.mockingDetails(mockedList).isSpy)
mockedList[0]
with(mockingDetails(mockedList)) {
println("typeToMock : ${mockCreationSettings.typeToMock}")
println("defaultAnser : ${mockCreationSettings.defaultAnswer}")
println("invocations : $invocations")
println("stubbings : $stubbings")
}
}
//result
isMock : true
isSpy : false
typeToMock : interface java.util.List
defaultAnser : RETURNS_DEFAULTS
invocations : [list.get(0);]
stubbings : [list.get(0); stubbed with: [Returns: hello]]