ScalaTestとScalaMockでテストを行なう
はじめに
今後Scalaを触ることになりそうになのでちょっと勉強しておこうかと思いまして、まずはテストのやり方を確認しようかと思いScalaTestとScalaMockを使ってユニットテストを書いてみようと思います。
書いてみる
環境
今回の環境は以下の通り
$ scala --version Scala code runner version 3.0.0 -- Copyright 2002-2021, LAMP/EPFL $ sbt --version sbt version in this project: 1.5.2 sbt script version: 1.5.2
セットアップ
まず、sbtのプロジェクトはIntelliJのプラグインで作成しました。
ScalaTestのセットアップはこちらを参考に行います。
まずは、build.sbt
に以下の依存を追加します。
libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.9" libraryDependencies += "org.scalamock" %% "scalamock" % "5.1.0" % Test libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % "test"
sbtで依存を定義するための手っ取り早い方法はlibraryDependenciesに次のような構文で記述するようです。
libraryDependencies += groupID % artifactID % revision % configuration
ここでは3つ依存を定義しています。scalatest
とscalamock
に関しては良いとしてscalaitc
ですが、ScalaTestの姉妹ライブラリーで==
オペレーターなどのテストやプロダクションコードで使えるような諸々を提供してくれるみたいです。今回使うかはわかりませんが、ドキュメントで依存に追加することが推奨されていたので追加しておこうと思います。
ScalaTestでユニットテストを記述する
ScalaTestのスタイルについて
ScalaTestでは以下のようなテストスタイルをサポートしているようです。
- FunSuiteスタイル
- FlatSpecスタイル
- FunSpecスタイル
- FreeSpecスタイル
それぞれのテストの書き方はこちらを参考にしてください。 最初のステップとしてはFlatSpecスタイルがおすすめされているようなので、このブログではFlatSpecを用いてテストを記述しようと思います。
単純なテストを書いてみる
まずは以下のような足し算をするだけの簡単なクラスを用意します。
class Calc { def plus(a: Int, b: Int): Int = a + b }
このクラスのメソッドplus
をテストするコードは以下のようになります。
import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should class CalcTest extends AnyFlatSpec with should.Matchers { it should "足し算の結果を返す" in { val c = new Calc c.plus(1, 2) should be (3) } }
AnyFlatSpec
はX should Y
や A must B
のような形式でテストを記述するための構文を提供してくれます。
should
やmust
、can
と行ったような助動詞の記述が可能なようです。
また、should.Matchers
をミクスインしていますが、これは result should be (expected)
のようにアサーションを記述するための構文を提供してくれます。
should.Matchers
は以下のような構文を提供します。
- result should equal (expected)
- 比較がカスタマイズ可能
- result should === (expected)
- 比較がカスタマイズ可能
- 型の制約を強制する
- result should be (expected)
- 比較がカスタマイズができない代わりにコンパイルが早い
- result shouldEqual expected
- ()を必要としない
- カスタマイズ可能な比較
- result shouldBe 3
- ()を必要としない
- 比較がカスタマイズができない代わりにコンパイルが早い
should.Matchers
の他にもAnyFlatSpec
の上位のクラスでミクスインされているAssertions
トレイトではアサーションを行なうためのいくつかのマクロを定義されています。
このマクロには例えば以下のようなものがあります。
- fail
- テストを失敗させる
- succeed
- テストを成功させる
他にもさまざまなマクロがあります。詳細はこちらを確認ください。
それでは、記述したテストを実行してみます。
様々な実行方法がありますが、今回はsbtを使って実行したいと思います。
単純にすべてのテストを実行するためにはプロジェクトルートで以下のコマンドを実行します。
$ sbt > test [info] CalcSpec: [info] - should 足し算の結果を返す [info] CalcFunSuiteTest: [info] - 足し算の結果を返す [info] Run completed in 120 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 2, aborted 0 [info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 0 s, completed 2021/05/29 23:15:25
CalcFunSuiteTest
はブログには記載してませんが、同じテストを別のスタイルで書いているだけです。ここではそんなに気にしなくても大丈夫です。
すべてのテストでは無く一部のテストを実行したい場合は以下のように実行します。
> testOnly CalcSpec [info] CalcSpec: [info] - should 足し算の結果を返す [info] Run completed in 324 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 1 s, completed 2021/05/29 23:14:45
先程と違いCalcSpec
だけが実行されたのがわかります。
例外をテストする
例外のテストを行なう場合もshould.Matchers
で定義されているshould be thrownBy
を用います。
例えば先程のplus
メソッドは正の数だけを受け取ることを想定しているメソッドで負の数を受け取った場合はIllegalArgumentException
を投げることを想定しているとします。
CalcSpec
に以下のテストを追加します。
it should "負の数が引数に渡された場合にIllegalArgumentException" in { val c = new Calc a[IllegalArgumentException] should be thrownBy { c.plus(1, -2) } }
このテストを実行すると以下の結果になります。
> testOnly CalcSpec [info] CalcSpec: [info] - should 足し算の結果を返す [info] - should 負の数が引数に渡された場合 *** FAILED *** [info] Expected exception java.lang.IllegalArgumentException to be thrown, but no exception was thrown (CalcSpec.scala:13) [info] Run completed in 118 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 1, canceled 0, ignored 0, pending 0 [info] *** 1 TEST FAILED *** [error] Failed tests: [error] CalcSpec [error] (Test / testOnly) sbt.TestsFailedException: Tests unsuccessful [error] Total time: 0 s, completed 2021/05/29 23:22:03
まだ実装をしてないので失敗しますね。
それでは実装を行ってもう一度テストを実行します。
実装を以下のように修正します。
class Calc { def plus(a: Int, b: Int): Int = { require(a > 0 && b > 0) a + b } }
Scalaの引数チェックはrequire
メソッドを用いて行なうことができるようです。
こいつは条件に合致しない場合、IllegalArgumentException
を投げます。
テストを実行します。
> testOnly CalcSpec [info] CalcSpec: [info] - should 足し算の結果を返す [info] - should 負の数が引数に渡された場合にIllegalArgumentException [info] Run completed in 123 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 0 s, completed 2021/05/29 23:29:27
実行が成功しましたね。
ScalaMockでモックする
次はScalaMockを使ってモックを行ってみようと思います。
例えば以下のようなトレイトとクラスがあったとします。
trait Language { def greeting(): String = "Hello!!" } class Person(val lang: Language) { def saySomeThing(): String = lang.greeting() } class Japanese extends Language { override def greeting(): String = "こんにちは" }
このPerson
クラスのsaySomeThing
メソッドをテストし、Language
トレイトをモックするとします。
テストは以下のように記述します。
class PersonSpec extends AnyFlatSpec with should.Matchers with MockFactory { it should "Languageをモックする" in { val mockLang = mock[Language] (mockLang.greeting _).expects().returning("Hello, ScalaMock!!").once() val target = new Person(mockLang) target.saySomeThing() should be ("Hello, ScalaMock!!") } }
MockFactory
をミクスインすることで、ScalaMockの構文を利用することができます。
まずは、val mockLang = mock[Language]
のところでMockオブジェクトを作成します。
次に、(mockLang.greeting _).expects().returning("Hello, ScalaMock!!").once()
のところで、モックの設定をしてます。
書いてあるとおりですがreturning
でモックのが返す値を
モックが引数を期待する場合はexpects()
の引数に渡すようです。
例えばなんでも良いがモックが1つの引数を期待する場合はexpects(*)
とかけば良いようです。
そして最後に、once()
ですがこのモックが1回実行されることを確認しています。
このテストを実行すると以下のような結果になります。
> testOnly PersonSpec [info] PersonSpec: [info] - should Languageをモックする [info] Run completed in 132 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 0 s, completed 2021/05/30 1:08:24
非同期でテストする
ScalaTestとScalaMockを非同期にするのは簡単でAsyncFlatSpec
とAsyncMockFactory
を利用するだけです。
先程のPersonTestのテストを非同期で行なう用に書き換えます。
import org.scalamock.scalatest.AsyncMockFactory import org.scalatest.flatspec.AsyncFlatSpec import org.scalatest.matchers.should class PersonSpec extends AsyncFlatSpec with should.Matchers with AsyncMockFactory { it should "Languageをモックする1" in { val mockLang = mock[Language] (mockLang.greeting _).expects().returning("Hello, ScalaMock!").once() val target = new Person(mockLang) target.saySomeThing() should be("Hello, ScalaMock!") } it should "Languageをモックする2" in { val mockLang = mock[Language] (mockLang.greeting _).expects().returning("Hello, ScalaMock!!").once() val target = new Person(mockLang) target.saySomeThing() should be("Hello, ScalaMock!!") } }
AnyFlatSpec
とMockFactory
を単純に置き換えただけです。
AnyFlatSpec
に対するAsyncFlatSpec
の用にスタイルごとに非同期用のものが用意されているようです。
実行すると以下のような結果になります。
> testOnly PersonSpec [info] PersonSpec: [info] - should Languageをモックする1 [info] - should Languageをモックする2 [info] Run completed in 151 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 0 s, completed 2021/05/30 1:10:53