自己紹介
- 五十嵐 智哉
- GitHub
- クーガー株式会社
- AI・Robotics関連の開発
- DeepLearning向け3DCG学習データ生成シミュレーター開発
- ロボット向けクラウドデータベース及び認識エンジンのためのサーバー・クライアント開発
- Scala/Play Framework, C#/Unity, C++, Python
この章では、Scalaにおけるアサーションやテスト方法についてみていきます。
Agenda
- アサーション(assertions)
- ScalaTest
- specs2
- ScalaCheck
アサーション(assertions)
アサーションとは
プログラムの前提条件を示すのに使われる。 アサーションするところで必ず真であるべき式の形式をとる。
Predef
シングルトンオブジェクトに定義されているassert
メソッドを使って条件が満たされているか確認できます。
assert((1 + 1) == 2)
assert((1 + 1) == 3, "hogehoge")
/*
java.lang.AssertionError: assertion failed: hogehoge
at scala.Predef$.assert(Predef.scala:170)
... 42 elided
*/
条件が満たされない場合、AssertionError
が投げられます。
アサーション(assertions)
関数の結果を返す直前でアサーションを行う場合、ensuring
というメソッドを使うと便利です。
def sum(i: Int, j: Int): Int = {
i + j
} ensuring (_ < 10, "over 10")
sum(1, 1)
sum(10, 20)
/*
java.lang.AssertionError: assertion failed: over 10
at scala.Predef$Ensuring$.ensuring$extension3(Predef.scala:261)
at .sum(<console>:11)
... 42 elided
*/
ScalaTest
ScalaTestは、様々なテストスタイルに適用できるScala製テストツール
import org.scalatest.FunSuite
class ScalaTestSampleSuite extends FunSuite {
test("1 + 1") {
val answer = 1 + 1
assert(answer == 3) // Test FAILED!!
}
}
/*
...
[info] ScalaTestSampleSuite:
[info] - 1 + 1 *** FAILED ***
[info] 2 did not equal 3 (ScalaTestSampleSuite1.scala:5)
...
*/
使い方は、任意のクラスでFunSuite
クラスを継承し、test
メソッドをコンストラクターで実行します。
そのtest
メソッドの引数に、タイトルとassert
を実行するための関数値を渡します。
ScalaTest
DiagrammedAssertions
トレイトを使うと、失敗時のエラーレポートをダイアグラム形式で表示することができます。
import org.scalatest.{DiagrammedAssertions, FunSuite}
class ScalaTestSampleSuite extends FunSuite with DiagrammedAssertions {
test("1 + 1") {
val answer = 1 + 1
assert(answer == 3) // Test FAILED!!
}
}
/*
[info] ScalaTestSampleSuite:
[info] - 1 + 1 *** FAILED ***
[info] assert(answer == 3)
[info] | | |
[info] 2 | 3
[info] false (ScalaTestSampleSuite.scala:5)
*/
ScalaTest
期待値と結果値をはっきりとさせたい場合、assertResult
を使います。
import org.scalatest.FunSuite
class ScalaTestSampleSuite extends FunSuite {
test("1 + 1") {
val answer = 1 + 1
assertResult(3) { // Test FAILED!!
answer
}
}
}
/*
...
[info] - 1 + 1 *** FAILED ***
[info] Expected 3, but got 2 (ScalaTestSampleSuite.scala:5)
...
ScalaTest
予想通りの例外が投げられたかチェックするにはassertThrows
を使います。
また、予想していた例外の詳細を知りたい時は、intercept
を使います。
import org.scalatest.{DiagrammedAssertions, FunSuite}
class ScalaTestSampleSuite extends FunSuite with DiagrammedAssertions {
test("1: List#apply") {
assertThrows[IllegalArgumentException] { // Test FAILED!!
val answer = List(1, 2, 3)
answer(3)
}
}
test("2: List#apply") {
val caught = intercept[IndexOutOfBoundsException] {
val answer = List(1, 2, 3)
answer(3) // Throw exception.
}
assert(caught.getMessage == "") // Test FAILED!!
}
}
/*
...
[info] - 1: List#apply *** FAILED ***
[info] Expected exception java.lang.IllegalArgumentException to be thrown,
but java.lang.IndexOutOfBoundsException was thrown (ScalaTestSampleSuite.scala:4)
...
[info] - 2: List#apply *** FAILED ***
[info] assert(caught.getMessage == "")
[info] | | | |
[info] | "3" | ""
[info] | false
[info] java.lang.IndexOutOfBoundsException: 3 (ScalaTestSampleSuite.scala:14)
...
やってみよう 1/4
実際にScalaTestを動かしてみましょう。
- 下記GitHubリポジトリからコード一式を取得。
scala_test_base_template
- リポジトリのルート直下に移動し、下記コマンドを実行。
sbt "test-only org.example.ScalaTestSampleSuite"
Behavior-Driven Development(BDD)テストスタイル
BDDとは
コードに対して期待されるふるまいを人間が理解できる仕様にまとめた上で、実際の
コードのふるまいがその仕様に合っているかを確かめるテストを実施すること。
ScalaTestでは、BDDテストスタイルを行いやすくするためのFlatSpec
トレイトがあります。
またMatchers
トレイトをmixinすることにより、自然言語に近い形でアサーションを記述することができます。
import org.scalatest.{FlatSpec, Matchers}
class ScalaTestSampleFlatSpec extends FlatSpec with Matchers {
val xs = List(1, 2, 3)
"List(1, 2, 3)" should "be three length" in {
xs.length should be(3)
}
it should "throw an IndexOutOfBoundsException if specified index 3" in {
an[IndexOutOfBoundsException] should be thrownBy {
xs(3)
}
}
}
Behavior-Driven Development(BDD)テストスタイル
specs2
specs2は、BDDテストスタイルをサポートするもうひとつのテストツール。
import org.specs2.mutable._
object Specs2SampleSpecification extends Specification {
val xs = List(1, 2, 3)
"List(1, 2, 3)" should {
"be length 3" in {
xs.length must_== 3
}
"throw an IndexOutOfBoundsException if specified out of index" in {
xs(3) must throwA[IndexOutOfBoundsException]
}
}
}
やってみよう 2/4
同じようにspecs2も動かしてみましょう。
- リポジトリのルート直下に移動し、下記コマンドを実行する。
sbt "test-only org.example.Specs2SampleSpecification"
やってみよう 3/4
整数の四則演算を行うクラスを作成し、そのクラスのテストを行ってください。
ゼロ除算でArithmeticException
が投げられることのテストも行ってください。
ScalaCheck
ScalaCheckは、自動的に任意のテストデータを作成し、それらのデータとテスト仕様が満たされているかをチェックすることができます。
import org.scalatest.WordSpec
import org.scalatest.prop.PropertyChecks
import org.scalatest.MustMatchers
class ScalaTestSampleWordSpec extends WordSpec with PropertyChecks with MustMatchers {
"Adding positive integer by Int" must {
"be positive integer" in {
forAll { (a: Int, b: Int) =>
whenever(a > 0 && b > 0) {
val answer = a + b
answer must be > 0 // overflow!!
}
}
}
}
"Adding positive integer by BigInt" must {
"be positive integer" in {
forAll { (a: BigInt, b: BigInt) =>
whenever(a > 0 && b > 0) {
val answer = a + b
answer must be > BigInt.int2bigInt(0)
}
}
}
}
}
やってみよう 4/4
先程の四則演算クラスについて、以下の法則をScalaCheckでテストしてください。
- 結合法則
- 交換法則
やってみよう解答編
package chapter14
class BasicArithmeticOperators(val num: Int) {
def +(that: BasicArithmeticOperators): BasicArithmeticOperators = new BasicArithmeticOperators(this.num + that.num)
def -(that: BasicArithmeticOperators): BasicArithmeticOperators = new BasicArithmeticOperators(this.num - that.num)
def *(that: BasicArithmeticOperators): BasicArithmeticOperators = new BasicArithmeticOperators(this.num * that.num)
def /(that: BasicArithmeticOperators): BasicArithmeticOperators = new BasicArithmeticOperators(this.num / that.num)
override def hashCode = num.##
override def equals(other: Any) = other match {
case that: BasicArithmeticOperators =>
(that canEqual this) && (this.num == that.num)
case _ =>
false
}
def canEqual(other: Any) = other.isInstanceOf[BasicArithmeticOperators]
}
package chapter14
import org.specs2.mutable._
class BasicArithmeticOperatorsSpecification extends Specification {
"BasicArithmeticOperators" >> {
"Positive case" >> {
val a = new BasicArithmeticOperators(1)
val b = new BasicArithmeticOperators(1)
"#+" >> {
val answer = a + b
val expect = new BasicArithmeticOperators(2)
answer must_== expect
}
"#-" >> {
val answer = a - b
val expect = new BasicArithmeticOperators(0)
answer must_== expect
}
"#*" >> {
val answer = a * b
val expect = new BasicArithmeticOperators(1)
answer must_== expect
}
"#/" >> {
val answer = a / b
val expect = new BasicArithmeticOperators(1)
answer must_== expect
}
}
"Nagative case" >> {
val a = new BasicArithmeticOperators(1)
val b = new BasicArithmeticOperators(0)
"divided by zero" >> {
(a / b) must throwA[ArithmeticException]
}
}
}
}
package chapter14
import org.scalatest.WordSpec
import org.scalatest.prop.PropertyChecks
import org.scalatest.MustMatchers
class BasicArithmeticOperatorsWordSpec extends WordSpec with PropertyChecks with MustMatchers {
"BasicArithmeticOperators" must {
"be commutative" in {
forAll { (a: Int, b: Int) =>
val aa = new BasicArithmeticOperators(a)
val bb = new BasicArithmeticOperators(b)
val answer1 = aa + bb
val answer2 = bb + aa
answer1 must be(answer2)
}
}
"be associative" in {
forAll { (a: Int, b: Int, c: Int) =>
val aa = new BasicArithmeticOperators(a)
val bb = new BasicArithmeticOperators(b)
val cc = new BasicArithmeticOperators(c)
val answer1 = (aa + bb) + cc
val answer2 = aa + (bb + cc)
answer1 must be(answer2)
}
}
}
}