2014.12.08
Play Framework(Play2)(Scala)のTips Part2:Specs2とMockitoでテストを書く(Advent Calendar 8日目)
この記事はアライドアーキテクツAdvent Calendar 8日目の記事です。
どうも。謎の刺客Tarou Yamadaさんにカレンダーを乗っ取られてしまった伊藤(係長)です。
気を取り直して、今回はSpecs2とMockitoを使ったユニットテストのお話を書きます。
■Specs2とは
Scala用のテスティングフレームワークです。BDD(振る舞い駆動開発)スタイルでテストケースを
記述する事ができます。
詳しくはこちらをご覧ください。
■Mockitoとは
簡単に言うとテスト時に邪魔になる(依存している)クラスをモック(ハリボテ)にして、
単体テストを書きやすくするモックフレームワークです。
詳しくはこちらをご覧ください。
■前提
こんな実装があったとして、HogeServiceのテストを書こうとします。
Fuga.scala
1 2 3 4 5 6 7 8 9 |
object Fuga { def findById(param: Long): Entity = { // DBに繋いでEntityを取得して返す処理 entity } def update(param1: Long, param2: String) = { // DBに繋いでなんかする処理 } } |
HogeService.scala
1 2 3 4 5 6 7 |
object HogeService { // ↓この関数をテスト対象とする↓ def doSomething(param: Long) = { val entity = Fuga.findById(param) Fuga.update(entity.id, entity.name) } } |
見ての通り、HogeService#doSomething()はFugaオブジェクトに依存しています。
そのままテストを書いて実行しようとすると、毎回DB接続が必要になってしまいます。
これではあまりよろしくないので、Fugaクラスをモックにします。
■モック化の準備
では早速Fugaをモックにしましょう、と行きたいところですが、
HogeServiceとFugaはobjectとして定義されています。
objectとは、Scalaで簡単にシングルトンを定義できるキーワードですね。
手軽にシングルトンを定義できるので、結構多用している方も多いのではないでしょうか。
このままではHogeServiceで使用しているFugaをモック化して差し込む事ができません。
(objectはモック化する事ができません。)
そこで、実装を次の通りに変更します。
Fuga.scala
1 2 3 4 5 6 7 8 9 10 |
object Fuga extends Fuga class Fuga { def findById(param: Long): Entity = { // DBに繋いでEntityを取得して返す処理 entity } def update(param1: Long, param2: String) = { // DBに繋いでなんかする処理 } } |
HogeService.scala
1 2 3 4 5 6 7 8 |
object HogeService extends HogeService class HogeService { val fuga: Fuga = Fuga // Fugaオブジェクトをvalで持つ def doSomething(param: Long) = { val entity = fuga.findById(param) // フィールドのfugaを使う fuga.update(entity.id, entity.name) // フィールドのfugaを使う } } |
実装の本体をclassで定義して、それを継承したobjectを定義しました。
HogeServiceでは、依存しているFugaをvalでフィールド定義します。
こうする事でFugaをモック化してHogeServiceに差し込む事が可能となります。
(※他にもやり方は何通りかあると思いますがその一例です。)
■テストコード
ここまででモックしてテストコードを書く準備が整いましたので、
実際にテストを書いてみます。
HogeServiceSpec.scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import org.junit.runner._ import org.specs2.mock.Mockito import org.specs2.mutable._ import org.specs2.runner._ @RunWith(classOf[JUnitRunner]) class GeneralSettingServiceSpec extends Specification with Mockito { "doSomething" should { "update" in { val mockFuga = mock[Fuga] // Fugaをモック化 // findByIdにが1Lが渡って来た場合はEntity(2L, "cobonas")を返すようにダマす mockFuga.findById(1L) returns Entity(2L, "cobonas") val target = new HogeService { override val fuga = mockFuga // モック化したFugaを差し込む } target.doSomething(1L) // テスト対象関数を実行 there was one(mockFuga).update(2L, "cobonas") // Fugaのupdate関数が指定の引数で1回だけ呼ばれている事を確認 } } } |
これでOKです。
Fuga#findById()とFuga#update()はモック化されていますので、DB接続は行われません。
ここではFuga#update()が正しい引数で呼ばれている事を確認しています。
■spy
Mockitoにはspyというスバラシイ機能があります。
これは、テスト対象のクラスの一部分だけモック化できるというものです。
(パーシャルモックと言います)
実際のコードを見てみましょう。
Hoge.scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
object Hoge extends Hoge class Hoge { // ↓この関数をテスト対象とする↓ def isHoge: Boolean = { isFuga || isPiyo } def isFuga: Boolean = { //設定ファイルを見てなんかしてBooleanを返す } def isPiyo: Boolean = { // DBに繋いでなんかしてBooleanを返す } } |
isHogeの結果は、isFugaかisPiyoの結果で決まります。
そのままisHogeをテストしようとすると、設定ファイルとDBを見に行ってしまうので
あまりよろしくありません。
■テストコード
spyを使う事で次のように書く事ができます。
HogeSpec.scala
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import org.junit.runner._ import org.mockito.Mockito._ // ←これインポートする import org.specs2.mock.Mockito import org.specs2.mutable._ import org.specs2.runner._ @RunWith(classOf[JUnitRunner]) class HogeSpec extends Specification with Mockito { "isHoge" should { "isFugaがtrueならtrue" in { val target = spy(new Hoge) // spyでテスト対象のクラスをパーシャルモック化 // isFugaはtrueを返すようにダマす // spyを使った場合は、通常のmockのようにreturnsを使用すると本当の実装が実行されてしまうので注意!! doReturn(true).when(target).isFuga val result = target.isHoge // テスト対象の関数を実行 result mustEqual true // 結果の検証 there was no(target).isPiyo // isPiyoが1度も呼ばれない事を確認 } "isPiyoがtrueならtrue" in { val target = spy(new Hoge) // spyでテスト対象のクラスをパーシャルモック doReturn(false).when(target).isFuga // isFugaはfalseを返すようにダマす doReturn(true).when(target).isPiyo // isPiyoはtrueを返すようにダマす val result = target.isHoge // テスト対象の関数を実行 result mustEqual true // 結果の検証 } } } |
これで同一クラス内に定義されている関数をダマして、設定ファイルやDBを見に行かなくてもテストできるようになりました。
今回紹介したのはSpecs2、Mockitoの機能の内、ほん〜の一部だけですので、
実際触って見て色々試してみると良いと思います。
明日は、最近乗りに乗っているGit大佐こと大佐の番です!!!
元Javaプログラマ。現在はScala/PlayでWeb開発と、SwiftでiOSアプリ開発をしています。 Unitテストとか書いてる時が一番楽しかったりします。