この記事は?
Mockito使用時にUnfinishedStubbingExceptionとなり、調査に時間がかかったためメモ
起きた事象
以下のエラーが発生したが、原因が分からなかった
Unfinished stubbing detected here:
location
E.g. thenReturn() may be missing.
Examples of correct stubbing:
when(mock.isOk()).thenReturn(true);
when(mock.isOk()).thenThrow(exception);
doThrow(exception).when(mock).someVoidMethod();
Hints:
1. missing thenReturn()
2. you are trying to stub a final method, which is not supported
3: you are stubbing the behaviour of another mock inside before 'thenReturn' instruction is completed
結論
モックをネストして書いて無いかチェックしましょう
(お恥ずかしいことに、thenReturnの中にモックを差し込んでいることに気づくのに時間がかかってしまったため少しでも参考になれば嬉しいです)
この事象が起きるケース
// チャネル名を取得するモック
val mockChannel = mock[MockChannel]
when(mockChannel.channelName).thenReturn("チャネル名")
// チャネルを取得するモック
val channelService = mock[ChannelService]
when(channelService.getChannel()).thenReturn(s"channel not found. channelName: ${mockChannel.channelName}")
チャネルを取得するモック(channelService)に注目してください。
thenReturnで定義されている文字列の中に
別の箇所でモックとして定義されている変数が展開されていることが分かります。
${mockChannel.channelName}
このケースの場合、モックの対象がネストしてしまっている状態となるため
UnfinishedStubbingExceptionがthrowされます。
具体的にはエラーメッセージで表示されている3番になります。
(エラーメッセージが凄く親切に説明されていると後から気付きました👀)
Hints:
- missing thenReturn()
- you are trying to stub a final method, which is not supported
3: you are stubbing the behaviour of another mock inside before 'thenReturn' instruction is completed
Mockitoのコードを追ってみる
最終的にエラーをthrowする箇所
実際にUnfinishedStubbingエラーが出力される箇所は以下
public void validateState() {
validateMostStuff();
// validate stubbing:
if (stubbingInProgress != null) {
Location temp = stubbingInProgress;
stubbingInProgress = null;
throw unfinishedStubbing(temp);
}
}
処理の流れ
Mockitoではwhenメソッドでモック対象のOngoingStubbingを返しており、内部的にモック状態の参照(mockingProgress)を保持しています。
public <T> OngoingStubbing<T> when(T methodCall) {
MockingProgress mockingProgress = mockingProgress();
mockingProgress.stubbingStarted();
@SuppressWarnings("unchecked")
OngoingStubbing<T> stubbing = (OngoingStubbing<T>) mockingProgress.pullOngoingStubbing();
if (stubbing == null) {
mockingProgress.reset();
throw missingMethodInvocation();
}
return stubbing;
}
whenメソッドでは、再生時にstubbing状態のスタブが無いかどうか予めチェックを行います。
この処理により、thenReturnの書き忘れなど定義が完了しきっていないスタブを検知することができます。
具体的には以下のvalidateStateメソッドで該当のバリデーション処理を行っています。
public void stubbingStarted() {
validateState();
stubbingInProgress = new LocationImpl();
}
public void validateState() {
validateMostStuff();
// validate stubbing:
if (stubbingInProgress != null) {
Location temp = stubbingInProgress;
stubbingInProgress = null;
throw unfinishedStubbing(temp);
}
}
例として、上記バリデーションは以下のような呼び出しを検知します。
// channelServiceのstubbingが始まっている状態で、mockChannelのstubbingを開始
when(channelService.getChannel()).thenReturn(s"channel not found. channelName: ${mockChannel.channelName}")
解決策
stubbingの開始前に変数として定義すると、mock対象のstubが被らないためUnfinishedStubbingExceptionとなりません。
// チャネル名を取得するモック
val mockChannel = mock[MockChannel]
when(mockChannel.channelName).thenReturn("チャネル名")
// チャネルを取得するモック
val channelService = mock[ChannelService]
val errorMessage = s"channel not found. channelName: ${mockChannel.channelName}"
when(channelService.getChannel()).thenReturn(errorMessage)
最後に
Mockitoでエラーがthrowされるケースが多岐にわたるため、意外と調査に時間がかかりました。
エラーメッセージをちゃんと読むべきだと再認識しました。
自分自身ちゃんと理解できているのか不安なので
誤っている部分がありましたら、是非ご指摘をお願い致します。
参考
以下の記事が非常に参考になりました
http://speakman.net.nz/blog/2019/04/07/mockito-unfinishedstubbingexception-in-tests/