この記事は Scala Advent Calendar 2017 の12日目です。
テスト結果を何かしらの形でレポート出力したい、カスタマイズしたいということはよくある(?)とおもいます。
今回はそれをScalaTestで実装する方法のご紹介です。
今回紹介するソースコードは以下のリポジトリに含まれています。
手順1: org.scalatest.Reporter
トレイトをmix-inしたクラスを定義する
- ScalaTestが
Reporter
トレイトとしてAPIを提供してくれているのでそれを利用します。 -
Reporter
トレイトは抽象メンバとしてapply
メソッドを持っています。 - コンストラクタ引数は空にしましょう。
package org.nomadblacky.scalatest_repoter_sample
import org.scalatest.Reporter
import org.scalatest.events.Event
class SampleReporter extends Reporter {
override def apply(event: Event): Unit = ???
}
手順2: apply
メソッドを実装する
-
apply(event: Event)
メソッドにScalaTestでの発生したイベントが流れてくる仕組みです。- テストの開始・成功・失敗、テストスイートの開始・終了…など
-
org.scalatest.Event
はsealed trait
となっており、各イベントが型として定義されています。
class SampleReporter extends Reporter {
override def apply(event: Event): Unit = event match {
case _: RecordableEvent =>
case _: ExceptionalEvent =>
case _: NotificationEvent =>
case TestStarting(ordinal, suiteName, suiteId, suiteClassName, testName, testText, formatter, location, rerunner, payload, threadName, timeStamp) =>
case TestSucceeded(ordinal, suiteName, suiteId, suiteClassName, testName, testText, recordedEvents, duration, formatter, location, rerunner, payload, threadName, timeStamp) =>
case TestFailed(ordinal, message, suiteName, suiteId, suiteClassName, testName, testText, recordedEvents, throwable, duration, formatter, location, rerunner, payload, threadName, timeStamp) =>
case TestIgnored(ordinal, suiteName, suiteId, suiteClassName, testName, testText, formatter, location, payload, threadName, timeStamp) =>
case TestPending(ordinal, suiteName, suiteId, suiteClassName, testName, testText, recordedEvents, duration, formatter, location, payload, threadName, timeStamp) =>
case TestCanceled(ordinal, message, suiteName, suiteId, suiteClassName, testName, testText, recordedEvents, throwable, duration, formatter, location, rerunner, payload, threadName, timeStamp) =>
case SuiteStarting(ordinal, suiteName, suiteId, suiteClassName, formatter, location, rerunner, payload, threadName, timeStamp) =>
case SuiteCompleted(ordinal, suiteName, suiteId, suiteClassName, duration, formatter, location, rerunner, payload, threadName, timeStamp) =>
case SuiteAborted(ordinal, message, suiteName, suiteId, suiteClassName, throwable, duration, formatter, location, rerunner, payload, threadName, timeStamp) =>
case RunStarting(ordinal, testCount, configMap, formatter, location, payload, threadName, timeStamp) =>
case RunCompleted(ordinal, duration, summary, formatter, location, payload, threadName, timeStamp) =>
case RunStopped(ordinal, duration, summary, formatter, location, payload, threadName, timeStamp) =>
case RunAborted(ordinal, message, throwable, duration, summary, formatter, location, payload, threadName, timeStamp) =>
case InfoProvided(ordinal, message, nameInfo, throwable, formatter, location, payload, threadName, timeStamp) =>
case AlertProvided(ordinal, message, nameInfo, throwable, formatter, location, payload, threadName, timeStamp) =>
case NoteProvided(ordinal, message, nameInfo, throwable, formatter, location, payload, threadName, timeStamp) =>
case MarkupProvided(ordinal, text, nameInfo, formatter, location, payload, threadName, timeStamp) =>
case ScopeOpened(ordinal, message, nameInfo, formatter, location, payload, threadName, timeStamp) =>
case ScopeClosed(ordinal, message, nameInfo, formatter, location, payload, threadName, timeStamp) =>
case ScopePending(ordinal, message, nameInfo, formatter, location, payload, threadName, timeStamp) =>
case DiscoveryStarting(ordinal, configMap, threadName, timeStamp) =>
case DiscoveryCompleted(ordinal, duration, threadName, timeStamp) =>
}
}
- ここでは
TestSucceeded
とTestFailed
のふたつを実装しましょう
class SampleReporter extends Reporter {
override def apply(event: Event): Unit = event match {
case e: TestSucceeded =>
println(s"OK: ${e.suiteName} ${e.testName} ${getLocation(e.location)}")
case e: TestFailed =>
println(s"NG: ${e.suiteName} ${e.testName} ${getLocation(e.location)} [${e.message}]")
case _ => // Do Nothing
}
private def getLocation(location: Option[Location]): String = location
.collect {
case TopOfClass(clazz) => s"$clazz"
case TopOfMethod(clazz, method) => s"$clazz#$method"
case LineInFile(lineNumber, fileName, _) => s"$fileName$$L$lineNumber"
}
.getOrElse("(Unknown)")
}
-
Event
のフィールドからテストの情報を取得できます。
手順3: テスト時に実装したReporterを使うよう build.sbt
を書き換える
- ScalaTestに実装したクラスを使うよう教えてあげます。
-
build.sbt
の適当な箇所に以下を追記します。- ScalaTestのテストランナーに
-C
オプションでReporterを渡しています。 - その他のオプションに関してはこちらを参照してください。
- ScalaTestのテストランナーに
testOptions in Test ++= Seq(
Tests.Argument(TestFrameworks.ScalaTest, "-C", "org.nomadblacky.scalatest_repoter_sample.SampleReporter")
)
手順4: テストを実行する
- 準備が整ったのでテストを実行してみましょう。
- てきとうなテストを用意します。
package org.nomadblacky.scalatest_repoter_sample
import org.scalatest._
class SampleTest extends FunSuite with Matchers {
test("Success01") {
1 + 1 shouldBe 2
}
test("Success02") {
1 - 1 shouldBe 0
}
test("Fail01") {
1 / 0 shouldBe Double.NaN
}
test("Success03") {
1 * 2 shouldBe 2
}
test("Fail02") {
val maybe: Option[Int] = None
maybe.get shouldBe 0
}
}
- 成功するテスト、失敗するテストをそれぞれ用意しました。
-
sbt test
を叩きましょう。
$ sbt test
(前略)
[info] Done compiling.
OK: SampleTest Success01 SampleTest.scala$L7
OK: SampleTest Success02 SampleTest.scala$L11
NG: SampleTest Fail01 (Unknown) [/ by zero]
OK: SampleTest Success03 SampleTest.scala$L19
NG: SampleTest Fail02 (Unknown) [None.get]
[info] Run completed in 704 milliseconds.
[info] Total number of tests run: 5
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 3, failed 2, canceled 0, ignored 0, pending 0
[info] *** 2 TESTS FAILED ***
[error] Failed tests:
[error] org.nomadblacky.scalatest_repoter_sample.SampleTest
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 9 s, completed 2017/12/10 19:13:08
- 期待する結果が得られました!
- (失敗したテストのLocationが上手く取れてないみたいですが…)
- あとは自分好みに実装してあげましょう!
余談
- こちらのREADME.mdを同様の方法で出力しています。
- sbtのtestListenersでも似たようなことができるかも?(よく調べてない)