This post is Private. Only a writer or those who know its URL can access this post.
More than 1 year has passed since last update.

自己紹介

  • 五十嵐 智哉
  • クーガー株式会社
  • AI・Robotics関連の開発
    • DeepLearning向け3DCG学習データ生成シミュレーター開発
    • ロボット向けクラウドデータベース及び認識エンジンのためのサーバー・クライアント開発
  • Scala/Play Framework, C#/Unity, C++, Python

この章では、Scalaにおけるアサーションやテスト方法についてみていきます。

Agenda

  1. アサーション(assertions)
  2. ScalaTest
  3. specs2
  4. ScalaCheck

アサーション(assertions)

アサーションとは

プログラムの前提条件を示すのに使われる。 アサーションするところで必ず真であるべき式の形式をとる。

Predefシングルトンオブジェクトに定義されているassertメソッドを使って条件が満たされているか確認できます。

REPL
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というメソッドを使うと便利です。

REPL
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製テストツール

ScalaTestSampleSuite.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トレイトを使うと、失敗時のエラーレポートをダイアグラム形式で表示することができます。

ScalaTestSampleSuite.scala
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を使います。

ScalaTestSampleSuite.scala
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を使います。

ScalaTestSampleSuite.scala
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を動かしてみましょう。

  1. 下記GitHubリポジトリからコード一式を取得。
    scala_test_base_template
  2. リポジトリのルート直下に移動し、下記コマンドを実行。
    sbt "test-only org.example.ScalaTestSampleSuite"

Behavior-Driven Development(BDD)テストスタイル

BDDとは

コードに対して期待されるふるまいを人間が理解できる仕様にまとめた上で、実際の
コードのふるまいがその仕様に合っているかを確かめるテストを実施すること。

ScalaTestでは、BDDテストスタイルを行いやすくするためのFlatSpecトレイトがあります。
またMatchersトレイトをmixinすることにより、自然言語に近い形でアサーションを記述することができます。

ScalaTestSampleFlatSpec.scala
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テストスタイルをサポートするもうひとつのテストツール。

Specs2SampleSpecification.scala
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も動かしてみましょう。

  1. リポジトリのルート直下に移動し、下記コマンドを実行する。
    sbt "test-only org.example.Specs2SampleSpecification"

やってみよう 3/4

整数の四則演算を行うクラスを作成し、そのクラスのテストを行ってください。
ゼロ除算でArithmeticExceptionが投げられることのテストも行ってください。


ScalaCheck

ScalaCheckは、自動的に任意のテストデータを作成し、それらのデータとテスト仕様が満たされているかをチェックすることができます。

ScalaTestSampleWordSpec.scala
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でテストしてください。

  • 結合法則
  • 交換法則

やってみよう解答編

BasicArithmeticOperators.scala
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]
}
BasicArithmeticOperatorsSpecification.scala
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]
      }
    }
  }
}
BasicArithmeticOperatorsWordSpec.scala
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)
      }
    }
  }
}
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.