관리 메뉴

JIE0025

[MockK] 코틀린을 위한 mocking library, 단위테스트 작성방법 본문

백엔드/테스트

[MockK] 코틀린을 위한 mocking library, 단위테스트 작성방법

Kangjieun11 2023. 6. 7. 23:44
728x90

✅ 개요

스프링 기반의 프로젝트에선, 코틀린을 사용해도 기존의 테스트 프레임워크인 JUnit, Assertion, Mockito를 동일하게 사용할 수 있지만..

 

그래도 코틀린에 어울리게 코틀린스럽게(?) 테스트코드를 짤 수 있도록 돕는 도구들이 존재한다. (MockK와 Kotest)

 

 

이 중 오늘은 MockK를 알아보고 어떻게 단위테스트를 하는지 간단하게 알아보자.

 

✍️ 기술 비교

MockK외에도 코틀린을 위한 테스팅 프레임워크에는 Kotest라는 것도 있다. 

  • Kotest는 코틀린진영에서 가장 많이 사용되는 테스팅 프레임워크이다.
  • 다양한 테스트 레이아웃을 제공하고 Kotlin DSL 스타일의 Assertion도 제공된다.
  • Junit과 호환성이 있어 기존의 Junit과 마이그레이션 하기 용이함.

예제 코드 - Kotest , 출처 : 우아한 테크 블로그

더보기

Kotest Behavior Spec을 활용한 기존의 Given When Then 정의 

internal class CalculatorBehaviorSpec : BehaviorSpec({
    val sut = Calculator()

    given("calculate") {
        val expression = "1 + 2"
        `when`("1과 2를 더하면") {
            val result = sut.calculate(expression)
            then("3이 반환된다") {
                result shouldBe 3
            }
        }

        `when`("수식을 입력하면") {
            then("해당하는 결과값이 반환된다") {
                calculations.forAll { (expression, answer) ->
                    val result = sut.calculate(expression)

                    result shouldBe answer
                }
            }
        }

        `when`("입력값이 null이거나 빈 값인 경우") {
            then("IllegalArgumentException 예외를 던진다") {
                blanks.forAll {
                    shouldThrow<IllegalArgumentException> {
                        sut.calculate(it)
                    }
                }
            }
        }

        `when`("사칙연산 기호 이외에 다른 연산자가 들어오는 경우") {
            then("IllegalArgumentException 예외를 던진다") {
                invalidInputs.forAll {
                    shouldThrow<IllegalArgumentException> {
                        sut.calculate(it)
                    }
                }
            }
        }
    }
}) {
    companion object {
        private val calculations = listOf(
            "1 + 3 * 5" to 20.0,
            "2 - 8 / 3 - 3" to -5.0,
            "1 + 2 + 3 + 4 + 5" to 15.0
        )
        private val blanks = listOf("", " ", "      ")
        private val invalidInputs = listOf("1 & 2", "1 + 5 % 1")
    }
}

 

 

✅ MockK

https://mockk.io/

 

 

Mockk는 코틀린을 위한 모킹라이브러리이다.

 

Mock, 가짜 객체를 만들어 단위테스트를 하던 Mockito에서

코틀린 스타일로 만들어진것이라고 이해하면 된다. 

 

 

 

MockK를 사용하면 코틀린DSL과 Infix를 사용하여 코틀린 스타일의 테스트코드를 작성할 수 있다.

 

 

 

 

✅ 단위 테스트

이전의 Mockito를 이용하여 단위테스트를 만들었던 방식을 순서로 나열했다.

 

0. 테스트 단위는 서비스 내부의 메서드이다. 

    -> 메서드를 테스트한다. 메서드를 테스트하기 때문에  input값이 있다면 미리 생성해두고(Given), 

비교할 output이 있다면 생성해둔다. 

 

1. mock(가짜) 객체를 생성한다 

    -> 서비스의 단위테스트이므로 서비스가 주입받는 Repository를 Mock객체로 생성하는것을 예로 들 수 있다. 

 

2. 가짜 객체를 통해 서비스를  Stubbing해서 처리한다. 

테스트할 대상 메서드는   서비스가 주입받았던 Bean을 실제로 호출하지 않고,  대신 가짜로 생성한 Mock객체를 생성하여 

테스트흐름을 차단한다.  대신 바로 리턴할 값을 지정해줌으로써 정상적으로 동작할 수 있게 만든다.

    -> 가짜 객체가 호출되었으므로 해당 객체가 반환할 값을 처리한다. (Stubbing의  return 값을 지정)

 

3. 서비스단 메서드가 끝났을 때 return value는 ,   미리 given에서 정의한 데이터와 같은지 검증(Verify)한다.

 

 


 

 

👩‍💻 테스트에 대한 의존성 추가

원하는 버전을 입력해주면 된다. 

testImplementation "io.mockk:mockk:${mockkVersion}"

 

implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.slf4j:slf4j-api:2.0.7")
testImplementation("io.mockk:mockk:1.13.5")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.slf4j:slf4j-simple:2.0.7")

 

 

Junit5에 MockK를 사용을 위한 어노테이션

@ExtendWith(MockKExtension::class) << 필수
class MockkTest { ... }

 

 

✅ 단위 테스트의 흐름을 따라가보자.

 

1. mock(가짜) 객체를 생성한다

    -> 서비스의 단위테스트이므로 서비스가 주입받는 Repository를 Mock객체로 생성하는것을 예로 들 수 있다. 

 

mock객체를 생성하는 방법을 아래와 같이 두가지 방법이 있다. 

생성 후 mock객체를 주입해준다


1) mockk()를 이용한 목객체 생성과 MappingService를 이용한 주입
private val userService = mockk<UserService>()
val mappingRepository: MappingRepository = mockk()


✅ 서비스를 테스트하는것이므로, 서비스를 변수로 선언하고, 목객체를 주입한다.
val mappingService = MappingService(mappingRepository)



2) 어노테이션을 이용한 mock객체 생성 방법

@MockK
lateinit var mappingRepository: MappingRepository

@Test
fun test() {
	val mappingService = MappingService(mappingRepository)
}

 

 

 

2. 가짜 객체를 통해 서비스를  Stubbing해서 처리한다.

테스트할 대상 메서드는   서비스가 주입받았던 Bean을 실제로 호출하지 않고,  대신 가짜로 생성한 Mock객체를 생성하여 

테스트흐름을 차단한다.  대신 바로 리턴할 값을 지정해줌으로써 정상적으로 동작할 수 있게 만든다.

    -> 가짜 객체가 호출되었으므로 해당 객체가 반환할 값을 처리한다. (Stubbing의  return 값을 지정)

 

 

아래 테스트 예시를 보면, mock객체를 생성하고,

실제로 서비스 메서드 (테스트 대상) 에서 mappingRepository.count()가 호출되면 무조건 100을 반환하도록 지정하여

데이터베이스로의 조회를 막았다.

 

@Test
fun test() {
    val mappingRepository: MappingRepository = mockk()

    every { mappingRepository.count() } returns 100

    println("mappingRepository.count() = ${mappingRepository.count()}") // mappingRepository.count() = 100
}

 

 

3. 서비스단 메서드가 끝났을 때 return value는 ,   미리 given에서 정의한 데이터와 같은지 검증(Verify)한다.

 

@Test
fun simpleTest() {
    val car = mockk<Car>() // mockk 객체 생성
 
    every { car.drive(NORTH) } returns OK // stubbing
 
    car.drive(NORTH) // 위에서 stubbing한 OK 리턴
 
    verify { car.drive(NORTH) } // 호출 검증
}

 

 

 

 

위와 같은 흐름으로 단위테스트가 진행된다

 

 

 

관련 메서드나 더 좋은 설명은 아래 블로그에 있으니 참고하자. 

 

https://techblog.woowahan.com/5825/

 

스프링에서 코틀린 스타일 테스트 코드 작성하기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요 저는 공통시스템개발팀에서 플랫폼 개발을 담당하고 있는 김규남이라고 합니다. 이 글은 올해 사내에서 진행한 코틀린 밋업에서 스프링에서 코틀린 스타일 테스트 코드

techblog.woowahan.com

 

 

 


 

references

https://techblog.woowahan.com/5825/

https://lannstark.tistory.com/229

https://effortguy.tistory.com/244

https://sabarada.tistory.com/191

https://team.mycaro.co.kr/android-mockk%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-unittest-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EB%B2%95-w-%EC%B9%B4%EB%A1%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%99%94%EB%A9%B4/

https://www.baeldung.com/kotlin/mockk