specs2のmock機能(stub機能)とtrait+overrideを使ってテスタビリティを上げる
2015/01/24
今回のネタは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を使ってみてください。