1. 序章

この短い記事では、Mockitoの代替と見なすこともできるテストモック用のScalaネイティブライブラリであるScalaMockの基本を紹介します。

2. 設定

ScalaMockは、ScalaTestおよびSpecs2と統合されています。

ScalaTestまたはSpecs2でScalaMockを使用するには、sbtファイルに依存関係を追加する必要があります。

libraryDependencies += "org.scalamock" %% "scalamock" % "5.1.0" % Test

3. 特徴

ライブラリをインストールしたので、ScalaMockの主要な機能のいくつかを説明してデモンストレーションしましょう。

3.1. 引数マッチング

引数のマッチングを実現する方法はいくつかあります。

まず、いくつかのクラスと関数を作成する必要があります。

// Models
case class Model1(id: Long, name: String)
case class Model2(id: Long, name: String)

// Services
trait UnitService1 {
  def doSomething(m1: Model1, m2: Model2): Unit
}

class UnitService2Impl(srv1: UnitService1) {
  def doSomething(m1: Model1, m2: Model2): Unit = {
    srv1.doSomething(m1, m2)
  }
}

クラスと関数を設定したら、モックを使用してテストケースを作成できます。

"UnitService2Impl" should {
  "call Service1 doSomething result" in {
    val m1 = Model1(1L, "Jim")
    val m2 = Model2(2L, "Timmy")
    val mockUnitService1 = mock[UnitService1]
    (mockUnitService1.doSomething _).expects(m1, m2)
    // (mockUnitService1.doSomething _).expects(m1, *) // * is the AnyMatcher, also succeeds
    // (mockUnitService1.doSomething _).expects(where { // functional/predicate matcher, also succeeds
    //  (_m1: Model1, _m2: Model2) => _m1.id == 1L && _m2.id == 2L
    //})
    val unitService2 = new UnitService2Impl(mockUnitService1)
    unitService2.doSomething(m1, m2)
    succeed
  }
}

上記の例では、上記の一致のいずれかが成功することに注意することが重要です。

もう1つの便利な機能は、イプシロンマッチングです。 EpsilonMatcher は、値に近い数値と一致します。

"EpsilonMatcher" should {
  case class Model3() {
    def numFunc(num: Float): Unit = {}
  }
  "match close numbers" in {
    val mockedModel3 = mock[Model3]
    (mockedModel3.numFunc _).expects(~71.5f)
    mockedModel3.numFunc(71.50002f) // success
    mockedModel3.numFunc(71.502f) // failure
    succeed
  }
}

3.2. 注文確認

もう1つの便利な機能は、モックされたインスタンスの実行順序を確認できることです。 この機能を紹介するには、サービスにコードを追加する必要があります。

trait UnitService1 {
  def doSomething(m1: Model1, m2: Model2): Unit
  def doSomethingElse(m1: Model1, m2: Model2): Unit
}

class UnitService2Impl(srv1: UnitService1) {
  def doSomething(m1: Model1, m2: Model2): Unit = {
    srv1.doSomething(m1, m2)
  }

  def doManyThings(m1: Model1, m2: Model2): Unit = {
    srv1.doSomething(m1, m2)
    srv1.doSomethingElse(m1, m2)
  }

  def doManyThingsInverted(m1: Model1, m2: Model2): Unit = {
    srv1.doSomethingElse(m1, m2)
    srv1.doSomething(m1, m2)
  }
}

それでは、注文検証を使用していくつかのテストケースを作成しましょう。

"Ordering" should {
    "verify that doSomething is called before doSomethingElse" in {
      val m1 = Model1(1L, "Jim")
      val m2 = Model2(2L, "Timmy")
      val mockUnitService1 = mock[UnitService1]
      inSequence {
        (mockUnitService1.doSomething _).expects(m1, m2)
        (mockUnitService1.doSomethingElse _).expects(m1, m2)
      }
      val unitService2 = new UnitService2Impl(mockUnitService1)
      unitService2.doManyThings(m1, m2) // success
      // unitService2.doManyThingsInverted(m1, m2) // failure
      succeed
    }

    "verify that doSomething and doSomethingElse are called in any order" in {
      val m1 = Model1(1L, "Jim")
      val m2 = Model2(2L, "Timmy")
      val mockUnitService1 = mock[UnitService1]
      inAnyOrder {
        (mockUnitService1.doSomething _).expects(m1, m2)
        (mockUnitService1.doSomethingElse _).expects(m1, m2)
      }
      val unitService2 = new UnitService2Impl(mockUnitService1)
      // unitService2.doManyThings(m1, m2) // also success
      unitService2.doManyThingsInverted(m1, m2) // success
      succeed
    }
  }

3.3. 通話数

さらに、ScalaMockを使用すると、モックされたインスタンス関数の呼び出し数を確認できます。 次のテストケースは、その方法を示しています。

"CallCounts" should {
  case class Model3() {
    def emptyFunc(): Unit = {}
  }
  "verify the number of calls" in {
    val mockedModel3 = mock[Model3]
    (mockedModel3.emptyFunc _).expects().once()
    // (mockedModel3.emptyFunc _).expects().twice() // exactly 2 times
    // (mockedModel3.emptyFunc _).expects().never() // never called
    // (mockedModel3.emptyFunc _).expects().repeat(4) // exactly 4 times
     (mockedModel3.emptyFunc _).expects().repeat(5 to 12) // between 5 and 12 times
    mockedModel3.emptyFunc()
    succeed
  }
}

3.4. 戻り値

モックのreturning関数を使用すると、関数の戻り値を制御できます。 returningを使用してケースを書いてみましょう。

"Returning" should {
  case class Model3() {
    def getInt(): Int = 3
  }
  "verify the number of calls" in {
    val mockedModel3 = mock[Model3]
    (mockedModel3.getInt _).expects().returning(12)
    assert(mockedModel3.getInt() === 12)
  }
}

3.5. 例外スロー

モック呼び出し中に例外をスローする必要がある場合は、 throwingmock関数を使用できます。 実際の動作を見てみましょう。

"ExceptionThrowing" should {
  case class Model3() {
    def getInt(): Int = 3
  }
  "throw exception on mock call" in {
    val mockedModel3 = mock[Model3]
    (mockedModel3.getInt _).expects().throwing(new RuntimeException("getInt called"))
    assertThrows[RuntimeException](mockedModel3.getInt())
  }
}

3.6. コールハンドラ

呼び出しハンドラーを使用すると、指定された引数に基づいて関数の戻り値を計算できます。 コールハンドラーを使用してテストケースを作成しましょう。

"CallHandlers" should {
  case class Model3() {
    def get(num: Int): Int = num - 1
  }
  "return argument plus 1" in {
    val mockedModel3 = mock[Model3]
    (mockedModel3.get _).expects(*).onCall((i: Int) => i + 1)
    assert(mockedModel3.get(4) === 5)
  }
}

3.7. 引数キャプチャ

Captureの実装で引数をキャプチャできます。 この機能は、発信者を制御できない場合に非常に便利です。

次の例は、ScalaMockを使用して引数をキャプチャする方法を示しています。

"ArgumentCapture" should {
  trait OneArg {
    def func(arg: Int): Unit
  }
  "capture arguments" in {
    val mockedOneArg = mock[OneArg]
    val captor = CaptureOne[Int]()
    (mockedOneArg.func _).expects(capture(captor)).atLeastOnce()
    mockedOneArg.func(32)
    assert(captor.value === 32)
  }
}

4. モッキングスタイル

前のセクションでは、同じモックスタイルを使用してさまざまな機能を示しました。 具体的には、期待を第一に考えたスタイルを使用しました。 ScalaMockを使用すると、期待値優先と記録後検証の2つのスタイルから選択できます。 特に、後者はMockitoを使用したことがある人にとってより馴染み深いものです。

誰かがどちらか一方を使用するか、両方を使用するかについての厳密な規則はありません。 記録してから検証するスタイルのテストケースを紹介しましょう。

"MockingStyle" should {
  trait MockitoWannabe {
    def foo(i: Int): Int
  }
  "record and then verify" in {
    val mockedWannabe = stub[MockitoWannabe]
    (mockedWannabe.foo _).when(*).onCall((i: Int) => i * 2)
    assert(mockedWannabe.foo(12) === 24)
    (mockedWannabe.foo _).verify(12)
  }
}

5. モック機能

5.1. シンプルな機能

mockFunction、を使用すると、クラスまたはtraitsと同様に関数をモックできます。 単純な関数をモックする方法を示しましょう。

"mock simple functions" in {
  val mockF = mockFunction[Int, Int]
  mockF.expects(*).onCall((i: Int) => i * 2).anyNumberOfTimes()

  assert(mockF.apply(1) === 2)
  assert(mockF.apply(11) === 22)
}

5.2. ポリモーフィック関数

ポリモーフィック関数をモックする必要がある場合は、期待値宣言でタイプを指定する必要があります。

"PolymorhicFunctions" should {
  "be mocked" in {
    trait Polymorphic {
      def call[A](arg: A): A
    }
    val mockPolymorphic = mock[Polymorphic]
    (mockPolymorphic.call[Int] _).expects(1).onCall((i: Int) => i * 2)
    assert(mockPolymorphic.call(1) === 2)
  }
}

5.3. オーバーロードされた関数

ポリモーフィック関数と同様に、オーバーロードされた関数は、引数の型を宣言するだけでモックできます。

"mock overloaded variants" in {
  trait Overloader {
    def f(i: Int): String
    def f(s: String): String
    def f(t: (Int, String)): String
  }
  val mockedOverloader = mock[Overloader]
  (mockedOverloader.f(_: Int)).expects(*).onCall((i: Int) => s"Int variant $i")
  (mockedOverloader.f(_: String)).expects(*).onCall((i: String) => s"String variant $i")
  (mockedOverloader.f(_: (Int, String))).expects(*).onCall((i: (Int, String)) => s"Tuple variant (${i._1}, ${i._2})")

  assert(mockedOverloader.f(1) === "Int variant 1")
  assert(mockedOverloader.f("str") === "String variant str")
  assert(mockedOverloader.f((1, "str")) === "Tuple variant (1, str)")
}

5.4. カレー関数

複数の引数リストを持つメソッドは、期待値呼び出しで各引数リストを埋めることによってモックされます。

"mock curried functions" in {
  trait CurryFunc {
    def curried(i: Int)(str: String): List[String]
  }
  val mockedCurryFunc = mock[CurryFunc]
  (mockedCurryFunc.curried(_: Int)(_: String)).expects(*, *).onCall((i, str) => Range(0, i).map(num => s"$str-$num").toList)
  assert(mockedCurryFunc.curried(2)("myStr") === List("myStr-0", "myStr-1"))
}

5.5. 暗黙の引数

暗黙の引数リストは、通常の引数リストと同様に期待値宣言に含める必要があります。 本質的に、 implicit 引数は、他の引数と同じように処理されます。

"use implicits" in {
  trait WithImplicit {
    def func(i: Int)(implicit j: BigDecimal): BigDecimal
  }
  val mockedWithImplicit = mock[WithImplicit]
  (mockedWithImplicit.func(_: Int)(_: BigDecimal)).expects(*, *).returning(BigDecimal(41))
  implicit val bigD = BigDecimal(12)
  assert(mockedWithImplicit.func(0) === BigDecimal(41))
}

6. Mockitoの比較

モックフレームワークを提示する場合、それをMockitoと比較するのは自然なことです。 record-then-verifyスタイルで記述されたテストケースはMockitoコードと非常によく似たコードを生成できますが、2つのフレームワークの間にはいくつかの重要な違いがあります。

最も重要な違いは、Mockitoは vals vars、およびクラスメンバーをモックできるのに対し、ScalaMockはできないことです。

次の例で、 vals vars 、およびクラスメンバーをMockitoでモックしてみましょう。

"Foo" should {
  case class Foo(i: Int) {
    val j: Int = 3
    val k: Int = 3
  }
  "mock vars, vals and class members" in {
    val mockedFoo = mock[Foo]
    when(mockedFoo.i).thenReturn(12)
    when(mockedFoo.j).thenReturn(14)
    when(mockedFoo.k).thenReturn(15)
    assert(mockedFoo.i === 12)
    assert(mockedFoo.j === 14)
    assert(mockedFoo.k === 15)
  }
}

ScalaMockは、reflection を使用するMockitoとは対照的に、マクロを使用して関数をオーバーライドします。 Scalaでは、 val lazy val 、または var を関数でオーバーライドできないため、ScalaMockの制限があります。

それどころか、関数をモックするためのScalaMock構文は、必要なコードが少なく、より直感的に見えます。 2つの比較を見てみましょう:

// Mockito
"MockitoBoilerplate" should {
  class Foo() {
    def call(f: Int => String, i: Int): String = f.apply(i)
  }
  "argument matcher for function should work" in {
    val mockedFoo = mock[Foo]
    when(mockedFoo.call(Matchers.any[Int => String].apply, Matchers.anyInt())).thenAnswer(new Answer[String] {
      override def answer(invocation: InvocationOnMock): String = {
        val intArg = invocation.getArgumentAt(1, classOf[Int])
        Range(0, intArg).mkString(",")
      }
    })
    assert(mockedFoo.call(_ => "bla", 3) === "0,1,2")
  }
}

// ScalaMock
"for mockito comparison" in {
  trait Foo {
    def call(f: Int => String, i: Int): String
  }
  val mockedFoo = mock[Foo]
  (mockedFoo.call _).expects(*, *).onCall((f: Int => String, i: Int) => Range(0, i).mkString(","))
  assert(mockedFoo.call(_ => "bla", 3) === "0,1,2")
}

7. 結論

この記事では、ScalaMockを使用した基本的なモックシナリオと高度なモックシナリオについて説明しました。 さらに、最も使用されているモックフレームワークであるMockitoと簡単かつ実用的な比較を行いました。

いつものように、上記の例のコードはGitHubから入手できます。