LL脳がscalaの勉強を始めたよ その40


コップ本も14章にはいってきましたよー、今回はテストに関するエトセトラですな

表明(assertions:アサーション)

assertionメソッド

前にも使ったassertionメソッドについてデス。assertionメソッドでは指定された条件に満たない場合にAssertionErrorを投げるものですねー

// x > 1に満たない場合にAssertionErrorを投げますよー
assert(x < 1)

またAssertionErrorに特定のメッセージを埋め込みたい場合は第2パラメーターに渡しますねー

assert(x < 1, "ダメですよ")

// val x = 3として実行してみました
scala> assert(x < 1, "ダメですよー")
// メッセージ埋込みですねー
java.lang.AssertionError: assertion failed: ダメですよー
	at scala.Predef$.assert(Predef.scala:92)
	at .<init>(<console>:6)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccess...
ensuringメソッド

演算後の結果値によって表明をおこないたい場合、assertionを使うと結果値を変数に格納してassertion判定することになるのでちょっと面倒だったりします。

// asserttionで結果値チェック
def hoge(h:Int) = {
 // 一度変数に格納しますよ
 val r = h + 5
 // 比較します
 assert(r < 10) 
}

そんな時はensuringメソッドを使いましょう!というお話です。ensuringは暗黙の型変換のおかげで任意の型の結果値使った述語関数を引数として取ることができます。引数の述語関数の中で結果値を扱う場合は "_"で表現しますねー

ちなみにensuringメソッドでもAssertrionErrorを発行しますねー

実際に上の内容を書き換えてみるとこんな感じですー

def hoge(h:Int) = {
 h + 5 
// 結果値を使って判定しますよー
} ensuring ( _ < 10)

ensuringメソッドを利用して実際にAssertionErrorを発行してみますかねー

// 結果値が10以上になってしまったのでAssertionErrorを発行しました(`・ω・´)
scala> hoge(6)
java.lang.AssertionError: assertion failed
	at scala.Predef$.assert(Predef.scala:87)
	at scala.Predef$Ensuring.ensuring(Predef.scala:132)
	at .hoge(<console>:6)
	at .<init>(<console>:6)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method...

ちなみにensuringメソッドでも第2パラメーターにエラーメッセージを指定できますねー

def hoge(h:Int) = {
 h + 5 
// 結果値を使って判定しますよー
} ensuring ( _ < 10, "エラーですよ")

// エラー発行です
scala> hoge(6)
java.lang.AssertionError: assertion failed: エラーですよ
	at scala.Predef$.assert(Predef.scala:92)
	at scala.Predef$Ensuring.ensuring(Predef.scala:133)
	at .hoge(<console>:7)
	at .<init>(<console>:6)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Nativ...
表明の有効、無効

jvmjavaコマンド)での実行時に-eaオプションを与えると有効、-daで無効になるみたいです。デフォルトでは無効っぽいですねー

Scalaにおける単体テスト

Scalaでの単体テストについてやりますよー

Scala単体テストを行うためにはJUnitTestNGなどのJavaツールを使う方法と、ScalaTest・specs・ScalaCheck等のScalaで書かれた新しいツールを使う方法があるみたいですね。

とりあえず一通り見ていきますかねー

ScalaTest

ScalaTestではテストを書く方法が複数あるみたいなんですが、とりあえず一番簡単な方法らしいorg.scalatest.Suiteを拡張してテストメソッドと定義する方法をやってみますよー

テスト環境のUbuntu10.04にはscalatestが含まれていなかったので、まずはscalatestをオフィシャルからDownloadしますね

ほんで対話コマンド実行時にクラスパスを通します

scala -cp <scalatestのjarファイル>

それでは実際にテストメソッドを書いてみましょー。テストメソッドはtestで始める必要がありマス

// テスト対象のクラスです
class Hoge {
  def moge = 1
}
// 単体テストクラスです
import org.scalatest.Suite
class HogeSuite extends Suite {
  def testMoge(){
    val h = new Hoge
    assert(h.moge < 2)
  }
}

んじゃテストを実行してみますかねー。テスト実行時にはSuiteを拡張したクラスのexcuteメソッドを実行します

scala> (new HogeSuite).execute()      
Test Starting - HogeSuite: testMoge
Test Succeeded - HogeSuite: testMoge

ちなみにHoge.mogeの値を3にした失敗パターンはこちらです

scala> (new HogeSuite).execute()
Test Starting - HogeSuite: testMoge
TEST FAILED - HogeSuite: testMoge (<console>:9)
  org.scalatest.TestFailedException
  ...
  at line3$object$$iw$$iw$HogeSuite.testMoge(<console>:9)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:616)
  at org.scalatest.Suite$$anon$5.apply(Suite.scala:1588)
  at org.scalatest.Suite$class.withFixture(Suite.scala:1509)
  ...
scalatest.FunSuite

scalatest.Suiteと別のスタイルのテストscalatest.FunSuiteも使ってみますかねー

FunSuiteではSuiteとは別にFunSuiteのtestメソッドに対してテスト用の関数を渡す形式でテストを実行するみたいですねー

// テスト対象のクラスです
class Hoge {
  def moge = 2
}
// 単体テストを定義しますよ
import org.scalatest.FunSuite
class HogeSuite extends FunSuite {
  // 実行テストを集約するメソッドです
  test("ダメなテスト結果ですよ"){
    val h = new Hoge
    assert(1 < h.moge, "testに直に記述した内容でエラーです")
    // 別名で定義したテストメソッドを実行しますよ
    mogetest()
  }
  // 別名のテストを定義しますねー
  def mogetest() {
    val h = new Hoge
    assert(h.moge < 2, "mogetestでエラーです")
  }
}

test内に直接テストコードを記述してもOKだし、別メソッドとして定義したテスト関数を渡してもいいみたいですね。しかも別メソッドにわけたテスト関数は命名制限されない…と。うん、パターンによってテスト内容を切り替えたりして使ってもいいですねー

んじゃ、実行してみます。実行はSuiteと同様にexecuteメソッドを使いますよー

scala> (new HogeSuite).execute
Test Starting - HogeSuite: ダメなテスト結果ですよ
TEST FAILED - HogeSuite: ダメなテスト結果ですよ (<console>:21)
  mogetestでエラーです
  org.scalatest.TestFailedException: mogetestでエラーです
  ...
  at line39$object$$iw$$iw$$iw$HogeSuite.mogetest(<console>:21)
  at line39$object$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:16)
  at line39$object$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:12)
  at org.scalatest.FunSuite$$anon$2.apply(FunSuite.scala:1158)
  at org.scalatest.Suite$class.withFixture(Suite.scala:1509)
  at line39$object$$iw$$iw$$iw$HogeSuite.withFixture(<console>:10)
  at org.scalatest.FunSuite$class.runTest(FunSuite.scala:1155)
  ...

ただし一箇所でエラーになるとその後のテストが行われないっぽいですなー。とりあえず全項目一斉テストをする場合はSuite、通しテストをする場合はFunSuiteみたいな使い分けがベストでしょうかねー

情報が豊富なエラーレポート

ScalaTestにはエラー情報表示につかえるオプションがいくつかあるので、そのご紹介をー

3連等号演算子

assertion等でエラー出力した際に、結果が等号条件を満たしていない旨を出力する場合は判定条件に3連等号演算子を使うと便利そうです。3連等号演算子を使うことで判定条件が満たされない場合はエラーレポート内に◯ did not equal △ のような出力をすることが出来るっぽいです。

ではやってみますかねー

// テスト対象のクラスです
class Hoge {
  def moge = 2
}
// 単体テストを定義しますよ
import org.scalatest.FunSuite
class HogeSuite extends FunSuite {
  //テストを実行します
  test("ダメなテスト結果ですよ"){
    val h = new Hoge
    // 判定条件をエラーレポートに出力します
    assert( h.moge === 3)
  }
}
// 実行しますよー
scala> (new HogeSuite).execute
Test Starting - HogeSuite: ダメなテスト結果ですよ
TEST FAILED - HogeSuite: ダメなテスト結果ですよ (<console>:15)
  // 等しくない旨のメッセージを出力します
  2 did not equal 3
  org.scalatest.TestFailedException: 2 did not equal 3
  ...
  at line46$object$$iw$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:15)
  at line46$object$$iw$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:13)
  at org.scalatest.FunSuite$$anon$2.apply(FunSuite.scala:1158)
  at org.scalatest.Suite$class.withFixture(Suite.scala:1509)
  at line46$object$$iw$$iw$$iw$$iw$HogeSuite.withFixture(<console>:11)
  at org.scalatest.FunSuite$class.runTest(FunSuite.scala:1155)
  at line46$object$$iw$$iw$$iw$$iw$HogeSuite.runTest(<console>:11)
  ...

結果をエラーレポートに出力できましたねー

expect

3連等号演算子だと判定条件の左辺と右辺が等しいかどうかの結果を出力するだけだったのですが、演算結果に明確な期待値がある場合に結果値と期待値が異なる旨を出力させたいときはexpectメソッドを使うといいみたいです。

expectメソッドでは期待値と結果値が異なる場合に「Expected ◯, but got △」のメッセージを出力できるみたいですねー

とりあえず出力してみますよー

// テスト対象のクラスです
class Hoge {
  def moge = 2
}
// 単体テストを定義しますよ
import org.scalatest.FunSuite
class HogeSuite extends FunSuite {
  //テストを実行します
  test("ダメなテスト結果ですよ"){
    val h = new Hoge
    // 結果値が期待値になっているかを判定しますよ
    expect(3){ h.moge }
  }
}

// エラーを出力しますねー
scala> (new HogeSuite).execute
Test Starting - HogeSuite: ダメなテスト結果ですよ
TEST FAILED - HogeSuite: ダメなテスト結果ですよ (<console>:16)
  // 結果値と期待値が違う旨を出力しました
  Expected 3, but got 2
  org.scalatest.TestFailedException: Expected 3, but got 2
  ...
  at line54$object$$iw$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:16)
  at line54$object$$iw$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:13)
  at org.scalatest.FunSuite$$anon$2.apply(FunSuite.scala:1158)
  at org.scalatest.Suite$class.withFixture(Suite.scala:1509)
  at line54$object$$iw$$iw$$iw$$iw$HogeSuite.withFixture(<console>:11)
  at org.scalatest.FunSuite$class.runTest(FunSuite.scala:1155)
  at line54$object$$iw$$iw$$iw$$iw$HogeSuite.runTest(<console>:11)
  ...
interceptメソッド

例外処理でエラーをキャッチしようとした場合に、予想と違うエラーが出力されることがあるそうですが、本来のエラーとして出力する予定だった例外が投げられたかどうかを判定するにはinterceptメソッドを使うみたいです

interceptメソッドでは予想されたエラーが投げられた場合にその旨のメッセージをエラーレポートに出力するみたいですねー

んじゃ予想と違うエラー投げてみますねー

// テスト対象のクラスです
class Hoge {}
// 単体テストを定義しますよ
import org.scalatest.FunSuite
class HogeSuite extends FunSuite {
  //テストを実行します
  test("ダメなテスト結果ですよ"){
     // IllegalArgumentExceptionが投げられたかどうかを判定します
     intercept[IllegalArgumentException]{
       // 例外を投げます
       throw new  RuntimeException
     }
  }
}

// エラーメッセージです
scala> (new HogeSuite).execute
Test Starting - HogeSuite: ダメなテスト結果ですよ
TEST FAILED - HogeSuite: ダメなテスト結果ですよ (<console>:11)
  // 予想したllegalArgumentExceptionじゃなくてRuntimeExceptionが起こってんじゃんって怒られます
  Expected exception java.lang.IllegalArgumentException to be thrown, but java.lang.RuntimeException was thrown.
  org.scalatest.TestFailedException: Expected exception java.lang.IllegalArgumentException to be thrown, but java.lang.RuntimeException was thrown.
  at org.scalatest.Assertions$class.newAssertionFailedException(Assertions.scala:279)
  at line129$object$$iw$$iw$$iw$HogeSuite.newAssertionFailedException(<console>:7)
  at org.scalatest.Assertions$class.intercept(Assertions.scala:487)
  at line129$object$$iw$$iw$$iw$HogeSuite.intercept(<console>:7)
  at line129$object$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:11)
  at line129$object$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:11)
  at org.scalatest.FunSuite$$anon$2.apply(FunSuite.scala:1158)
  at org.scalatest.Suite$class.withFixture(Suite.scala:1509)
  at line129$object$$iw$$iw$$iw$HogeSuite.withFixture(<console>:7)
  at org.scalatest.FunSuite$class.runTest(FunSuite.scala:1155)
  at line129$object$$iw$$iw$$iw$HogeSuite.runTest(<console>:7)
  at org.scalatest.FunSuite$$anonfun$runTests$1.apply(FunSuite.scala:1264)
  at org.scalatest.FunSuite$$anonfun$runTests$1.apply(FunSuite.scala:1255)
  at scala.List.foreach(List.scala:841)
  at org.scalatest.FunSuite$class.runTests(FunSuite.scala:1255)
  at line129$object$$iw$$iw$$iw$HogeSuite.runTests(<console>:7)
  at org.scalatest.Suite$class.run(Suite.scala:1804)
  at line129$object$$iw$$iw$$iw$HogeSuite.org$scalatest$FunSuite$$super$run(<console>:7)
  at org.scalatest.FunSuite$class.run(FunSuite.scala:1304)
  at line129$object$$iw$$iw$$iw$HogeSuite.run(<console>:7)
  at org.scalatest.Suite$class.execute(Suite.scala:1225)
  at line129$object$$iw$$iw$$iw$HogeSuite.execute(<console>:7)
  at line130$object$$iw$$iw$$iw$.<init>(<console>:9)
  at line130$object$$iw$$iw$$iw$.<clinit>(<console>)
  at RequestResult$line130$object$.<init>(<console>:3)
  at RequestResult$line130$object$.<clinit>(<console>)
  at RequestResult$line130$object.result(<console>)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:616)
  at scala.tools.nsc.Interpreter$Request.loadAndRun(Interpreter.scala:889)
  at scala.tools.nsc.Interpreter.interpret(Interpreter.scala:508)
  at scala.tools.nsc.Interpreter.interpret(Interpreter.scala:494)
  at scala.tools.nsc.InterpreterLoop.interpretStartingWith(InterpreterLoop.scala:242)
  at scala.tools.nsc.InterpreterLoop.command(InterpreterLoop.scala:230)
  at scala.tools.nsc.InterpreterLoop.repl(InterpreterLoop.scala:142)
  at scala.tools.nsc.InterpreterLoop.main(InterpreterLoop.scala:298)
  at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:141)
  at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)
  Cause: java.lang.RuntimeException
  at line129$object$$iw$$iw$$iw$HogeSuite$$anonfun$1$$anonfun$apply$1.apply(<console>:13)
  at line129$object$$iw$$iw$$iw$HogeSuite$$anonfun$1$$anonfun$apply$1.apply(<console>:13)
  at org.scalatest.Assertions$class.intercept(Assertions.scala:480)
  at line129$object$$iw$$iw$$iw$HogeSuite.intercept(<console>:7)
  at line129$object$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:11)
  at line129$object$$iw$$iw$$iw$HogeSuite$$anonfun$1.apply(<console>:11)
  at org.scalatest.FunSuite$$anon$2.apply(FunSuite.scala:1158)
  at org.scalatest.Suite$class.withFixture(Suite.scala:1509)
  at line129$object$$iw$$iw$$iw$HogeSuite.withFixture(<console>:7)
  at org.scalatest.FunSuite$class.runTest(FunSuite.scala:1155)
  at line129$object$$iw$$iw$$iw$HogeSuite.runTest(<console>:7)
  at org.scalatest.FunSuite$$anonfun$runTests$1.apply(FunSuite.scala:1264)
  at org.scalatest.FunSuite$$anonfun$runTests$1.apply(FunSuite.scala:1255)
  at scala.List.foreach(List.scala:841)
  at org.scalatest.FunSuite$class.runTests(FunSuite.scala:1255)
  at line129$object$$iw$$iw$$iw$HogeSuite.runTests(<console>:7)
  at org.scalatest.Suite$class.run(Suite.scala:1804)
  at line129$object$$iw$$iw$$iw$HogeSuite.org$scalatest$FunSuite$$super$run(<console>:7)
  at org.scalatest.FunSuite$class.run(FunSuite.scala:1304)
  at line129$object$$iw$$iw$$iw$HogeSuite.run(<console>:7)
  at org.scalatest.Suite$class.execute(Suite.scala:1225)
  at line129$object$$iw$$iw$$iw$HogeSuite.execute(<console>:7)
  at line130$object$$iw$$iw$$iw$.<init>(<console>:9)
  at line130$object$$iw$$iw$$iw$.<clinit>(<console>)
  at RequestResult$line130$object$.<init>(<console>:3)
  at RequestResult$line130$object$.<clinit>(<console>)
  at RequestResult$line130$object.result(<console>)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:616)
  at scala.tools.nsc.Interpreter$Request.loadAndRun(Interpreter.scala:889)
  at scala.tools.nsc.Interpreter.interpret(Interpreter.scala:508)
  at scala.tools.nsc.Interpreter.interpret(Interpreter.scala:494)
  at scala.tools.nsc.InterpreterLoop.interpretStartingWith(InterpreterLoop.scala:242)
  at scala.tools.nsc.InterpreterLoop.command(InterpreterLoop.scala:230)
  at scala.tools.nsc.InterpreterLoop.repl(InterpreterLoop.scala:142)
  at scala.tools.nsc.InterpreterLoop.main(InterpreterLoop.scala:298)
  at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:141)
  at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)


ちなみに予想通りのエラーが投げられればテスト成功です

// テスト対象のクラスです
class Hoge {}
// 単体テストを定義しますよ
import org.scalatest.FunSuite
class HogeSuite extends FunSuite {
  //テストを実行します
  test("ダメなテスト結果ですよ"){
     // IllegalArgumentExceptionが投げられたかどうかを判定します
     intercept[IllegalArgumentException]{
       // 例外を投げます
       throw new  IllegalArgumentException
     }
  }
}

// テスト実行します(テストが成功します)
scala> (new HogeSuite).execute
Test Starting - HogeSuite: ダメなテスト結果ですよ
Test Succeeded - HogeSuite: ダメなテスト結果ですよ

こんな感じで例外処理のチェックが行えるんですねー、うん、覚えておこうc⌒っ゚д゚)っφ メモメモ...

scalatestはとても便利

3連等号演算子やらexpectやらinterceptはScalaTestによって提供されているので、ScalaTestを読み込んで使ってくださいねーとのことです。

いじょー

ScalaTestをやったので次回はJUnitTestNGなんかをやりたいと思いますよー