mockito-scalaについて調べてみた

ScalaTestでは以前からMockitoSugarというトレイトが提供されており、ScalaからMockitoをちょっとだけ便利に使用することができるようになっていたのですが、元々それほど大した機能もなかった上に現在は別ライブラリに切り出されてしまったこともあり、使うモチベーションがだいぶ薄れてしまったのではないかと思います。実際のところ、ScalaTestのMockitoSugarを使わずに直接Mockitoを使っても大差ないのですが、やはりScalaからMockitoを使っていると不便に感じる点があったりします。

Mockito本家ではmockito-scalaというScala向けのライブラリが開発されており、こちらはScala向けにかなり作り込まれているようなので軽く試してみました。

github.com

このライブラリを使うにはbuild.sbtに以下の依存関係を追加します。Mockito本体はmockito-scalaの依存関係で引っ張ってくるので明示的に追加する必要はありません。

libraryDependencies ++= Seq(
  "org.mockito" %% "mockito-scala" % "1.16.0" % "test"
)

基本的な使い方はMockitoSugarArgumentMatchersSugarというトレイトをテストケースにミックスインします。

import org.scalatest.funsuite.AnyFunSuite
import org.mockito.{ ArgumentMatchersSugar, MockitoSugar }

class MyTest extends AnyFunSuite 
    with MockitoSugar 
    with ArgumentMatchersSugar {
  ...
}

もしくはmockito-scalaではトレイトに対応するシングルトンオブジェクトも用意されており、トレイトをミックスインする代わりにシングルトンオブジェクトのメンバをインポートすることで同じように使うことができます。

import org.scalatest.funsuite.AnyFunSuite
import org.mockito.ArgumentMatchersSugar._
import org.mockito.MockitoSugar._

class MyTest extends AnyFunSuite {
  ...
}

mockito-scalaでは以下のようにMockitoをScalaから使う際に便利な様々な機能が提供されています。

  • ScalaTestのMockitoSugarと同じようにmock[T]でモックを生成できる
  • 同様に例外スローや引数のキャプチャもdoThrow[T]Captor[T]のように記述できる
  • mock[MyClass with MyTrait]のようにモックの生成時にトレイトをミックスインできる
  • Mockitoのオーバーロードされた可変長引数メソッドの呼び出しが簡単にできる(doReturn(value, Nil: _*)Java同様doReturn(value)と記述できる)
  • spyLambdaで関数のspyが可能
  • verifyでデフォルト引数や名前渡し(遅延評価)の引数も適切に扱うことができる
  • eqがScalaTestのMatcherと衝突してしまう問題が解消されている(予めeqToが用意されている)
  • Matcherの型変数や括弧を省略可能(any[String]anyStringの代わりにanyと記述できる)
  • null撲滅のためにw Null matcherに警告が出る
  • 引数なしの関数のためにfunction0 matcherが用意されている
  • Value ClassのためにeqToVal matcherが用意されている
  • ArgumentCaptorの代わりにシンプルかつValue ClassをサポートしたArgCaptorが提供されている
  • MockitoのStrict Stubsのサポート(Idiomatic Syntaxではデフォルト)
  • ScalaTest用の便利トレイト(MockitoFixtureResetMocksAfterEachTestなど)
  • Answerを関数で記述できるようになっており、Invocationから引数を抽出する代わりに関数の引数として受け取ることができる
  • Idiomatic SyntaxやExpect DSLというマクロを活用したDSLが提供されている

最後のIdiomatic Syntaxですが、たとえば通常のMockitoを使用した次のようなコードがあるとします。

when(aMock.bar) thenReturn "mocked!"
when(aMock.baz(any)) thenReturn "mocked!"

verify(aMock, times(6)).bar
verify(aMock, atLeast(6)).baz(any)  

Idiomatic Syntaxではこのコードを次のような感じで記述することができるというものです。語順が異なるだけでなく、anyの代わりに*が使えたりします。

aMock.bar returns "mocked!"
aMock.baz(*) returns "mocked!"

aMock.bar wasCalled 6.times
aMock.baz(*) wasCalled atLeast(6.times)

Expect DSLはこのIdiomatic Syntaxをさらに変形させたようなもので、Idiomatic Syntaxで次のように記述するところを

aMock.bar wasCalled 6.times
aMock.baz(*) wasCalled atLeast(6.times)

expectから始まるDSLで置き換えることができます。

expect exactly 6.calls to aMock.bar
expect atLeast 6.calls to aMock.baz(*)

先頭にexpectが来るのでverifyしていることがわかりやすいということのようですが、それなら元のMockitoのverifyでいいのではという気がしなくも…。

その他、機能の細かい部分についてはmockito-scalaのREADMEや、mockito-scalaの作者であるBruno Bonannoさんのブログでも開設されています。

medium.com

mockito-scalaはかなり機能豊富でScalaからMockitoを使う際に便利になっている部分も多いのですが、Idiomatic SyntaxやExpect DSLは若干やりすぎ感がなくもない感じがします。Mockitoというより別のモックライブラリを使っている感じというか…。とはいえMockitoの知識なしで使えるかというとそれも微妙な感じなので、結局学習コストが二重にかかるだけなのではという懸念があります。また、マクロが活用されているということもあり、もし今後万が一mockito-scalaのメンテが止まってしまった場合の対応もなかなか大変そうな予感がします。

それ以外の部分については、特にValue Classやデフォルト引数、名前渡し引数のサポートやAnswerを関数と書けるようになっているあたりはなかなかポイント高いのではないかと思うので、独自DSLを使わず基本機能だけ使うのもありなのではないかと思いました。