티스토리 뷰

Spring

[Test] Mockito 톺아보기 w.kotlin

CharlieZip 2023. 6. 13. 01:00
반응형

테스트 공부를 위한 4번째 글입니다.

이번에 공부해볼 주제는 Mockito 도구입니다.

Mockito는 테스트를 편리하게 도와주는 도구로 Mock(가짜)객체를 쉽게 만들고 관리할 수 있게 도와주는 프레임워크로 설명이 필요없을 정도로 정말 많이 사용하는 프레임워크입니다.

이번에 저는 kotlin 언어를 이용해서 Mockito를 공부해 보겠습니다.

 

테스트 코드 공부 시리즈

1. AssertJ를 이용한 테스트 코드 작성

2. JUnit의 Parameterized 어노테이션 사용하기

3. Spring AutoConfigure Annotation Test

4. Mockito 톺아보기

5. kotlin에 특화된 mockito-kotlin 사용하기

6. postman을 이용한 시나리오 테스트 하기

 

Mock


Mock 객체 생성

Mock 객체란 실제 객체가 아닌 가짜 객체를 만들어서 테스트의 효용성을 높이는데 사용하는 객체입니다.

 

mock객체를 생성하는 방법은 간단합니다.

val mockedList = mock(mutableListOf<String>()::class.java)

 

@Mock 어노테이션

mock 객체 생성은 어노테이션을 이용해서도 가능합니다.

@Mock
private lateinit var database: ArticleDatabase

 

Stub


Stubbing하기

stubbing이란 mock 객체의 행동을 정하는 것입니다.

val mockedList = mock(LinkedList<String>()::class.java)

`when`(mockedList[0]).thenReturn("first")
`when`(mockedList[1]).thenThrow(RuntimeException::class.java)

// "first" 출력
println("mockedList[0] = ${mockedList[0]}")

// RuntimeException 예외 발생
println("mockedList[1] = ${mockedList[1]}")

// stubbing 되지 않은 경우는 null 반환
println("mockedList[999] = ${mockedList[999]}")

Stub은 when 메서드를 이용해서 할 수 있습니다.

코틀린의 경우 when 문법이 이미 존재하기 때문에 mockito.when()을 사용하려면 `문자를 양쪽에 추가해줘야 합니다.

 

Stub 지원 메서드

  • thenReturn : Stubbing 메서드 호출 후 어떤걸 리턴할지 정의
  • thenThrow : Stubbing 메서드 호출 후 어떤 Exception을 발생 할건지 정의

 

Stub을 할 때는 몇가지 주의 사항이 있습니다.

  1. stubbing이 되지 않은 메서드를 호출 하는 경우에는 null, empty collection 등을 반환합니다.
    • mockedList[999] 의 경우 stubbing한 메서드가 아니기 때문에 null을 반환하게 됩니다.
  2. 한번 stubbing 하면 이후에 몇번을 호출하더라도 같은 값을 반환합니다.
  3. 같은 메서드의 stubbing을 여러번 진행한다면 제일 마지막에 stubbing한 값을 반환합니다.
`when`(mockedList[0]).thenReturn("first")
`when`(mockedList[0]).thenReturn("two")

// "two" 출력
println("mockedList[0] = ${mockedList[0]}")

 

 

연속적인 Stubbing 하기

그렇다면 같은 메서드를 여러가지 응답이 하도록 하고 싶을때는 어떻게 해야할까요??

 

이러한 경우에 사용하는 스타일을 iterator-style stubbing 이라고 하는데요.

사용방법은 간단합니다. chaining 방식으로 선언하여 사용하면 됩니다.

`when`(mockedList[1])
    .thenReturn("one")
    .thenThrow(RuntimeException::class.java)

// stubbing 한 순서대로 값을 반환한다.
println(mockedList[1])  // one
println(mockedList[1])  // RuntimeException

 

만약 thenReturn 값만 여러가지로 응답한다면 아래와 같이 사용할 수 있습니다.

`when`(mockedList[1]).thenReturn("one", "two", "three")

// stubbing 한 순서대로 값을 반환한다.
println(mockedList[1])  // one
println(mockedList[1])  // two
println(mockedList[1])  // three

 

 

리턴값이 없는 메서드 Stubbing

컴파일러가 괄호 안에 void 메서드가 들어가는 것을 좋아하지 않기 때문에 when(Object)와 같은 형태로 void 메서드의 호출을 stubbing 하기 위해서는 다른 접근방법을 사용해야 합니다

val mockedList = mock(mutableListOf<String>()::class.java)

doThrow(RuntimeException::class.java).`when`(mockedList).clear()

mockedList.clear()  // RuntimeException

기존 thenReturn, thenThrow와 다르게 doReturn, doThrow를 사용하며 when메서드 앞에 위치해 사용해야 합니다.

 

Argument matchers

여태까지는 정적인 호출에 대한 Stubbing만 가능했습니다.

 

내가 `when`(mockedList[0]).thenReturn("first") 로 0번 인덱스에 대해 Stubbing을 진행했다면 0번 인덱스를 제외한 1번 인덱스 또는 99번 인덱스 등등 다른 경우에는 정상적인 값이 반환이 되지 않습니다.

만약 0번 인덱스 뿐만 아니라 어떤 인덱스를 호출해도 값을 반환하거나 좀 더 다양한 경우를 Stubbing 하고 싶은 경우에는 Argument matchers를 사용하면 됩니다.

 

val mockedList = mock(mutableListOf<String?>()::class.java)

`when`(mockedList[anyInt()]).thenReturn("element")

println("mockedList[1] = ${mockedList[1]}")  // element
println("mockedList[999] = ${mockedList[999]}") // element

ArgumentMatchers의 anyInt()를 사용하면 어떤 Int값이든 “element” 값을 리턴하도록 Stubbing이 가능합니다.

 

ArgumentMatchers는 anyInt(), anyString() 등등 여러가지 타입도 지원하고 있습니다.

 

다만, Stubbing할 메서드의 인자가 여러개라면 ArgumentMatchers 사용시 주의해야 할 사항이 있습니다.

 

먼저 여러 인자의 경우 기본적인 사용 방법은 동일합니다.

val mock = mock(Person::class.java)
`when`(mock.isDeveloper(anyString(), anyInt())).thenReturn(true)
assertThat(mock.isDeveloper("A", 1)).isTrue
assertThat(mock.isDeveloper("B", 1)).isTrue

 

주의해야 할 부분은 여러 인자들 중에 하나라도 ArgumentMatchers를 사용한다면

나머지 모든 인자들도 ArgumentMatchers를 사용해야 한다는 점 입니다.

// 기본 String 타입을 이용해 "charlie"를 선언해 InvalidUseOfMatchersException 예외 발생
`when`(mock.isDeveloper("charlie", anyInt())).thenReturn(false)

// ArgumentMatchers의 eq() 메서드를 사용해 특정 값을 지정
`when`(mock.isDeveloper(eq("charlie"), anyInt())).thenReturn(false)

 

 

 

Verify


verify는 단어 그대로 정상적으로 잘 작동하는지 검증하는 기능입니다.

 

기본 검증

val mockedList = mock(mutableListOf<String>()::class.java)

mockedList.add("once")

verify(mockedList).add("once") // 성공
verify(mockedList).add("twice") // 실패

 

time 옵션 검증

time옵션 검증이란 verify 메서드를 이용해서 Stubbing한 메서드가 몇번 실행됐는지 횟수에 관해서 검증을 진행합니다.

val mockedList = mock(mutableListOf<String>()::class.java)

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");

verify(mockedList, never()).add("never happened");

verify(mockedList, atMostOnce()).add("once");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");

각 메서드의 검증 방법

  • time(N) : N번 호출되었는지 검증, default 값은 1
  • atMostOnce() : 최대 한번 호출되었는지 검증, 0번 또는 1번
  • atLeastOnce() : 적어도 한번 호출되었는지 검증, 1번 또는 그이상
  • atLeast(N) : N번 이상 호출되었는지 검증
  • atMost(N) : N번 이하 호출되었는지 검증
  • never() : 한번도 호출 되지 않았는지 검증

 

inOrder 호출 순서 검증

호출 순서를 검증하기 위해서는 inOrder 객체를 생성해주면 됩니다.

val singleMock = mock(mutableListOf<String>()::class.java)

singleMock.add("was added first")
singleMock.add("was added second")

val inOrder = inOrder(singleMock)

// 메서드 호출 순서 검증
inOrder.verify(singleMock).add("was added first")
inOrder.verify(singleMock).add("was added second")

 

만약 하나의 객체가 아닌 여러 객체의 순서 검증이 필요한 경우에도 간단하게 검증이 가능합니다.

val firstMock = mock(mutableListOf<String>()::class.java)
val secondMock = mock(mutableListOf<String>()::class.java)

firstMock.add("was called first")
secondMock.add("was called second")

val inOrder = inOrder(firstMock, secondMock)

inOrder.verify(firstMock).add("was called first")
inOrder.verify(secondMock).add("was called second")

 

불필요한 invocations 검증

val mockedList = mock(mutableListOf<String>()::class.java)

mockedList.add("one")

verify(mockedList).add("one")

// verify 이후에 추가 interaction이 있었는지 검증한다.
verifyNoMoreInteractions(mockedList)

// 아무런 인터렉션이 없는지 검증한다.
verifyNoInteractions(mockedList) // fail

verifyNoMoreInteractions() 메서드를 사용하게 되면 추후 테스트 수정시에 인터렉션을 추가하지 못하게 됩니다.

즉, 테스트 유지보수에 제한적이고 어려움이 생길 수 있기 때문에 테스트 코드를 강력하게 제한하고 싶은신 경우가 아니라면 사용하실때 신중하게 생각하고 사용하시는걸 추천드립니다.

 

verifyNoInteractions() 메서드와 유사한 never() 이라는 메서드도 존재합니다.

never()메서드는 위에 time 옵션 검증에서 살펴보았는데요.

예를 들어 A -> B -> C 의 흐름으로 진행하는 프로그램에서

B 시점에 강제로 에러를 발생시켰을때 C가 실행이 되지 않았는지를 검증하는 방식으로도 사용할 수 있습니다.

 

두 메서드의 차이점은 아래와 같습니다.

  • verifyNoInteraction()는 객체, 즉 인스턴스 대상으로 검증을 진행
  • never() 메서드는 객체가 아닌 객체 내부의 특정 메서드를 대상으로 검증을 진행

 

 

Argument Capturing

argument capturing은 검증시에 사용하는 값을 검증하기 위한 목적으로 사용합니다.

class Person {
    fun checkAge(age: Int): Int {
        return 10
    }

    fun isDeveloper(name: String?, age: Int): Boolean {
        return true
    }
}
----------
val mock = mock(Person::class.java)

// ArgumentCaptor 선언, 검증시에 사용하는 값의 타입도 지정 필요
val argument = ArgumentCaptor.forClass(Int::class.java)

mock.checkAge(20)
verify(mock, times(1)).checkAge(argument.capture())

assertThat(20).isEqualTo(argument.value)

verify를 통해 checkAge 메서드를 검증할때 사용하는 값을 argument.capture()를 통해 얻을 수 있습니다.

그리고 argument.capture로 얻은 값은 argument.value를 통해 검증이 가능합니다.

 

 

만약, 검증해야 하는 값이 여러개라면 allValues 메서드를 이용해서 검증이 가능합니다.

mock.checkAge(20)
mock.checkAge(30)
verify(mock, times(2)).checkAge(argument.capture())

val expected = argument.allValues
assertThat(20).isEqualTo(expected[0])
assertThat(30).isEqualTo(expected[1])

 

 

Spy


spy객체는 spy()를 이용하여 생성 합니다.

spy객체는 mock객체와 다르게 가짜 객체를 만드는게 아닌 실제 객체를 사용합니다.

val mutableListOf = LinkedList<String>()
val spy = spy(mutableListOf)

`when`(spy.size).thenReturn(100)

// mutableListOf 객체의 실제 add() 메서드 사용
spy.add("one")
spy.add("two")

println(spy[0]) // one

// stubbing 한 메서드는 stubbing 한 값을 사용
println(spy.size) // 100

// verify 검증
verify(spy).add("one")
verify(spy).add("two")

 

다만, spy()는 실제 객체를 사용하기 때문에 실제 객체가 불가능한 동작은 stubbing이 불가능합니다.

val mutableListOf = LinkedList<String>()
val spy = spy(mutableListOf)

// 실제 객체가 empty 한 상태여서 get 메서드 사용시 IndexOutOfBoundsException 에러를 반환하기 때문에 stubbing이 불가능하다.
`when`(spy[0]).thenReturn("one")  // 실패

doReturn("one").`when`(spy)[0] // 성공

doReturn으로 어떻게 stubbing이 가능한지 알려면 thenReturn과 doReturn의 차이점을 살펴볼 필요가 있습니다.

 

thenReturn

  • 메서드를 실제 호출하지만 리턴 값은 임의로 정의 할 수 있다.
  • 메서드 작업이 오래 걸릴 경우 끝날때까지 기다려야함
  • 실제 메서드를 호출하기 때문에 대상 메서드에 문제점이 있을 경우 발견 할 수 있다.

doReturn

  • 메서드를 실제 호출하지 않으면서 리턴 값을 임의로 정의 할 수 있다.
  • 실제 메서드를 호출하지 않기 때문에 대상 메서드에 문제점이 있어도 알수가 없다.

 

doReturn의 경우 실제 메서드를 호출하지 않기 때문에 유효하지 않은 경우에도 stubbing이 가능하게 됩니다.

doReturn을 사용하면 메서드 작업을 기다리지 않아도 되어서 테스트의 성능이 더 빨라질 수 잇는 장점도 있지만

대상 메서드의 문제점을 발견할 수 없다는 단점 때문에 저는 주로 thenReturn을 사용하고 있습니다.

 

 

마무리

Mockito에 대해 기본적인 부분들만 공부해서 정리해보았습니다.

제가 작성한 내용 외에도 given,when,then 스타일로 코드를 짤 수 있게 도와주는 BDDMockito, 테스트 코드 작성시 에러가 발생할 경우 유용하게 사용할 수 있는 MockingDetails(Mocking에 대한 상세정보를 출력)와 같은 기능들도 존재하기 때문에 여유가 되신다면 공식문서를 한번 읽어보시는걸 추천드립니다.

 

사실 Mockito는 자바를 기반으로 하는 프레임워크이기 때문에 코틀린언어로 호환이 다 가능은 하지만 약간 불편한 부분들이 존재합니다.

이러한 불편을 해결하기 위해 나온 mockito-kotlin이 있는데요. mockito-kotlin에 대해서는 다음 글에서 알아보겠습니다.

 

긴글 읽어주셔서 감사합니다!!

 

참고자료

https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#26

 

Mockito - mockito-core 5.2.0 javadoc

Latest version of org.mockito:mockito-core https://javadoc.io/doc/org.mockito/mockito-core Current version 5.2.0 https://javadoc.io/doc/org.mockito/mockito-core/5.2.0 package-list path (used for javadoc generation -link option) https://javadoc.io/doc/org.m

javadoc.io

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함