specs2のmock機能(stub機能)とtrait+overrideを使ってテスタビリティを上げる


今回のネタはScala + specs2 + mockitoを使ったmockによるユニットテストです。

なるべくテストしやすい形でテストコードを書いて行きます!

前提

build.sbtに以下を追加するだけです。

libraryDependencies ++= Seq(
  "org.specs2" %% "specs2-core" % "2.4.15" % "test",
  "org.specs2" %% "specs2-mock" % "2.4.15" % "test"
)

mockを使う場合はmockのdependencyの追加を忘れずに。

今回は最新版を使いますが、Play frameworkなどと一緒に利用する場合はspecs2の依存ライブラリとの競合にご注意ください。

コード

まずは普通に書いてみます。(ダメな例)

こういうControllerクラス、Serviceクラスがあるという想定で、

import org.joda.time.DateTime

object FooController {
  def getDateTime: String = {
    val now = BarService.dateString
    
    "now : " + now
  }
}

object BarService {
  def dateString: String = {
    val now = DateTime.now()
    now.toString("yyyy_MM_dd")
  }
}


テストを書いてみます。

class FooControllerSpec extends Specification {
  
  "FooControllerSpec#getDateTime" should {
    "今日の日付が取得できること" in {
      val expected = "now : 2015_01_17"
      val actual = FooController.getDateTime

      expected must be equalTo actual
    }
  }
}

テスト対象の関数はnow : という文字列とともに整形された日付を返します。

ダメな点

これは非常に悪いテストコードです。

なぜならControllerがServiceに依存しているにも関わらず、そこをstubとして書いていません。Controllerのテストを書きたいのに、実質Serviceのテストまで書いてしまっているような状況です。

さらに根本的な問題として、ServiceのdateStringは外部リソースに依存しています。この場合は現在日時です。つまり、今回のテストケースは日付が変わると失敗し始めます・・・・。外部リソースはDBなどのケースが多いですね。その場合もまた、テストレコードの用意とかで結構苦労します。

trait + overrideを使ってテスタビリティを上げつつ、mockを使ってテストの安定性を上げる

さて、ここからは上記のダメな例を改善して行きます。

まず初めにControllerとServiceはtraitにしてしまいます。さらに、Serviceのobjectを呼び出すのではなく、それをController内部に変数として保持しておく形式にします。

import org.joda.time.DateTime

trait FooController {
  
  val barService: BarService = BarService
  
  def getDateTime: String = {
    val now = barService.dateString
    
    "now : " + now
  }
}

object FooController extends FooController

trait BarService {
  def dateString: String = {
    val now = DateTime.now()
    now.toString("yyyy_MM_dd")
  }
}

object BarService extends BarService

これだけ見ると、意味あんの・・・?という感じですが、これで大分テスタビリティが上がります。理由はテストコードを書くときにbarServiceを適宜設定することで、stubを使える為です。

では、テストコードを書いて行きます。

import org.specs2.mock.Mockito
import org.specs2.mutable.Specification

class FooControllerSpec extends Specification with Mockito {
  
  "FooControllerSpec#getDateTime" should {
    "今日の日付が取得できること" in {
      val expected = "now : 2010_01_01"
      
      val controller = new FooController {
        override val barService: BarService = {
          val m = mock[BarService]
          m.dateString returns "2010_01_01"
        }
      }
      val actual = controller.getDateTime

      expected must be equalTo actual
    }
  }
}

ポイントはmockという部分でモックオブジェクトを作成している点です。returnsで返却値を固定しています。

ここで作った物をoverride valしつつ、new FooControllerとすることで、Serviceの挙動を書き換えたControllerができます。これをテストに使うことで単純にFooContollerのロジックだけをテストすることができます。

どうでしょうか、意外と簡単だったかと思います。

FooController内部で利用するモジュールが増えれば増えるほど、mockを用意する手間が増えるのが難点ですが、これで外部リソース依存のテストなどを避けることができます。

ちなみに自分は今やっているプロジェクトで、初めはメモリDB + テスト用SQLという形でテストコードを書いていたんですが、テストデータがこんがらがって意味不明になってしまったので、今は上記の方式を取っています。

みなさんもぜひspecs2でmockitoを使ってみてください。