LoginSignup
6
2

More than 5 years have passed since last update.

ScalaTestで独自のテストレポートを実装する

Last updated at Posted at 2017-12-12

この記事は 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.Eventsealed 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) =>
  }
}
  • ここでは TestSucceededTestFailed のふたつを実装しましょう
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を渡しています。
    • その他のオプションに関してはこちらを参照してください。
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が上手く取れてないみたいですが…)
  • あとは自分好みに実装してあげましょう!

余談

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2