ScalaMockを使ってみよう

ScalaMockScalaネイティブなモックライブラリです。ScalaではJavaベースのモックライブラリを使用することもできますが(実際、SpecsはMockitoとの連携機能を提供しています)、ScalaMockはシングルトンやfinalクラス、関数のモックを作ることができるという特徴があります。どうやっているのかというと、Scalaコンパイラプラグインを使ってモックのソースコードを生成するという荒業を使っているようです。なので、使用する際にはいろいろと準備が必要になります。
作者の方のブログでも以下のエントリでScalaMockのチュートリアルが紹介されていますが、これをベースに実際に試してみての注意点などを含めScalaMockの使い方を紹介してみようと思います。

なお、現在のところ、ScalaMockはScalaTestと組み合わせて使うことが想定されているようです。リポジトリを見るとSpecs2と連携するためのコードもすでに存在するようなので近いうちにSpecs2でも利用できるようになるのかもしれません。また、最新のScalaMockはsbt 0.11専用となっています。

sbtの準備

まずはproject/project/PluginDef.scala(ファイル名は.scalaであればなんでもいいです)を以下のような内容で作成します。sbt 0.10まではプラグインの設定はproject/pluginsディレクトリで行っていたのですが、0.11ではproject/projectディレクトリで行うようです。project/pluginsディレクトリがあるとproject/projectディレクトリの設定が読み込まれないので注意してください。

import sbt._
object PluginDef extends Build {
  override lazy val projects = Seq(root)
  lazy val root = Project("plugins", file(".")) dependsOn(scalamockPlugin)
  lazy val scalamockPlugin = uri("git://github.com/paulbutcher/scalamock-sbt-plugin")
}

あと、プラグインはgitリポジトリから取得してコンパイルが行われるため、最初の一回はgitが実行できる環境であること、上記で指定しているgitリポジトリに接続できることが必要になります。
続いてproject/MyBuild.scala(これもファイル名は.scalaであればなんでもOK)を以下のような内容で作成します。プロジェクト定義に加えてScalaMockの依存関係とコンパイラプラグインの登録を行っています。orgzanizationとかプロジェクト名とかは適当に変えてください。

import sbt._
import Keys._
import ScalaMockPlugin._

object MyBuild extends Build {

  override lazy val settings = super.settings ++ Seq(
    organization := "jp.sf.amateras.scala",
    version := "1.0",
    scalaVersion := "2.9.1",
    resolvers += ScalaToolsSnapshots,
    libraryDependencies += "org.scalamock" %% "scalamock-scalatest-support" % "2.1",
    autoCompilerPlugins := true,
    addCompilerPlugin("org.scalamock" %% "scalamock-compiler-plugin" % "2.1")
  )

  lazy val myproject = Project("scalamock", file(".")) settings(generateMocksSettings: _*) configs(Mock)
}

とりあえずここまででsbtの準備は完了です。

モックの生成

次にコンパイラプラグインでどのモックを生成するのかを指定するためにsrc/generate-mocks/scala/GenerateMocks.scalaを作ります。このほかに関数のモックを生成するためのmockFunctionというアノテーションもあります。

import org.scalamock.annotation.mock
import org.scalamock.annotation.mockObject

class GenerateMocks {
  // 普通のクラスのモックを作る
  @mock[sample.HelloWorld]
  class Dummy1
  
  // シングルトンのモックを作る
  @mockObject(sample.StringUtils)
  class Dummy2
}

そしたら「sbt generate-mocks」でモックを生成します。Eclipseを使っている場合は生成されたモックのソースコードをソースパスに追加するためにここで一度「sbt eclipse」を再実行しておくとよいでしょう(じゃないとEclipse上でモックを使ったテストケースがコンパイルエラーになってしまいます)。

モックを使ったテストケースを書く

ここまできたらようやくモックを使ったテストケースを書けるようになります。以下の例はテストケースになっていませんが、モックに期待値と戻り値を設定できることを試すために書いたコードなのでご容赦ください。

package sample
import org.scalatest.FunSuite
import org.scalamock.annotation.mock
import org.scalamock.scalatest.MockFactory
import org.scalamock.generated.GeneratedMockFactory

class ScalaMockTest extends FunSuite 
    with MockFactory with GeneratedMockFactory {

  // 普通のクラスのモックを使ってみる
  test("HelloWorld") {
    val helloworld = mock[HelloWorld]
    helloworld.expects.hello("World") returning ("Hello World!")

    val result = helloworld.hello("World")
    assert(result === "Hello World!")
  }

  // シングルトンのモックを使ってみる
  test("StringUtils") {
    val util = mockObject(StringUtils)
    util.expects.isEmpty("") returning (false)

    val result = StringUtils.isEmpty("")
    assert(result === false)
  }
}

生成されたモックのソースコードはtarget/scala-x.x.x/src_managed/tstディレクトリ配下にありますので、どんな感じになっているのか知りたい場合は見てみるとよいと思います。

まとめ

シングルトンのモックを作れるのは確かに便利です。便利なのですが、正直手間がかかりすぎかなと思いました。設定自体は一度やってしまえば終わりですが、本体を直した場合はモックの再生成も必要になりますし、将来的にsbtのバージョンアップの障害となってしまう可能性も否定できません。
というわけで、コードを書くときにシングルトンにテスト不可能な(テストを書く際に実装の差し替えが必要となるような)処理を書かないように留意していれば大抵のケースはMockitoでカバーできると思うので、わざわざScalaMockを使わなくてもいいんじゃないかなぁ…というのが個人的な感想です。