MockKExceptionが発生する場合、必ず同例外でテストが落ちると思っていた話

概要

モックライブラリの一つであるMockKを使用していたところ、relaxed周りで当初の認識と違う挙動があったため備忘録として書いています。

発生した問題

当初の理解

class Class1 {
  fun methodUnit(): Unit {}
  fun methodNotUnit(): Int { return 1 }
}

// 通常のMock
val mock1: Class1 = mockk()
mock1.methodUnit() // exception
mock1.methodNotUnit() // exception

// relaxUnitFun = trueのMock
val mock2: Class1 = mockk(relaxedUnitFun = true)
mock1.methodUnit() // exception
mock1.methodNotUnit() // exception

// relaxed = trueのMock
val mock3: Class1 =  mockk(relaxed = true)
mock1.methodUnit() 
mock1.methodNotUnit() 

上記のように、relaxをtrueにしていない箇所では即MockKExceptionが返却され、必ずテストが失敗すると思っていました。

失敗せずにテストが続行されるケース

class HogeViewModel(): ViewModel {
  val exceptionHandler = CoroutineExceptionHandler { _,_ -> }
  fun method(hoge: Hoge) {
     viewModelScope(exceptionHandler).launch { 
       hoge.call()
       // do something
     }
  }
}

// test file
fun test() = runTest {
  val hoge: Hoge = mockk()
  viewModel.method(hoge)

  // 他の処理やアサーション
}

上記のテストコードにおいて、Hogeクラスはrelaxedなモックオブジェクトではないため、callメソッドを呼んだタイミングでMockKExceptionによってテストが失敗しそうです。 しかし、同コードを実行すると、他の処理やアサーションに進んでしまいます。

そのため、当初はモックオブジェクト以外に原因があると思い込んでしまいました。

原因

原因はViewModelScopeとCoroutineExceptionHandlerです。 SuperVisorJobを持つViewModelScopeとCoroutineExceptionHandlerがあることによって、scope内で発生した例外は親Coroutineに伝播しません。 つまり、runTestで生成されたCoroutineには伝播せず、MockKExceptionが発生していることを認識しづらくなっています。

そもそもMockKはどうやって動いているのかの参考

zenn.dev