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を使ってみてください。