CLOVER🍀

That was when it all began.

ScalaTest 2.2.0のDiagrammedAssertionsを試してみる

ScalaTest 2.2.0がリリースされたようです。2.2.0の変更内容でちょっと気になるものに、「DiagrammedAssertions」というのがあったので試してみました。

2.2.0の変更内容はRelease Notesを見ていただければと思うのですが、

ScalaTest/Scalactic 2.2.0 Release Notes
http://www.scalatest.org/release_notes/2.2.0

今回は

  • Enhanced Assertions error messages
  • DiagrammedAssertions

を取り扱います。

いや、せっかくなのでメッセージの改善というのも見てみようと思いまして。

準備

とりあえず、2.1.7と比較しつつやろうかなと思ったので、以下のようなsbtの設定ファイルを用意しました。
build.sbt

name := "scalatest-2.2.0-improve"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.1"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

incOptions := incOptions.value.withNameHashing(true)

libraryDependencies ++= Seq(
  "org.scalatest" %% "scalatest" % "2.1.7" % "test"
  //"org.scalatest" %% "scalatest" % "2.2.0" % "test"
)

これで、ScalaTest 2.1.7と2.2.0を切り替えつつ遊んでみたいと思います。

メッセージの改善

実際にプログラムを書いたりRelease Notesを見ていて思ったのですが、これもそうですがDiagrammedAssertionsも、基本的にはassert関係の話なんですねぇ…。

普段ScalaTestを使っている時は、Matchersを使っている方なのですが…。

まあ、いいんですけど。そういえば、今回見ていて気付いたのですが、assertってマクロなんですね。

で、とりあえず、こんなテストケースを書いてみました。
src/test/scala/org/littlewings/scalatest/ScalaTestSampleSpec.scala

package org.littlewings.scalatest

import org.scalatest.FunSpec

class ScalaTestSampleSpec extends FunSpec {
  describe("Assertions Improve spec") {
    it("compare values") {
      val (a, b, c, d) = (1, 2, 3, 4)

      assert(a == b || c >= d)
    }

    it("List#exists#1") {
      val xs = List(1, 2, 3)

      assert(xs.exists(_ == 4))
    }

    it("string test") {
      assert("hello".startsWith("h") && "goodbye".endsWith("y"))
    }

    it("instanceOf") {
      val num = 1.0

      assert(num.isInstanceOf[Int])
    }

    it("Some#isEmpty") {
      assert(Some(2).isEmpty)
    }

    it("None#isDefined") {
      assert(None.isDefined)
    }

    it("List#exists#2") {
      val xs = List(1, 2, 3)

      assert(xs.exists(i => i > 10))
    }
  }
}

ええ、かなりRelease Notes的な感じです!このテストは、すべて失敗します。

そういえば、Assertions使って書くの、初めてですね…。

では、ScalaTest 2.1.7で動かした結果を、一部載せてみます。

> test

結果。

[info] ScalaTestSampleSpec:
[info] Assertions Improve spec
[info] - compare values *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException was thrown. (ScalaTestSampleSpec.scala:13)

〜以下、省略〜

…おお、マジですか?これだけだったんだ!

では、2.1.7から2.2.0でどう変わったか、テストコード自体は一切触らずにsbtでの依存関係の変更のみでどれだけ変わるか、見てみましょう。

まあ、これを

libraryDependencies ++= Seq(
  "org.scalatest" %% "scalatest" % "2.1.7" % "test"
  //"org.scalatest" %% "scalatest" % "2.2.0" % "test"
)

こうするだけですね。

libraryDependencies ++= Seq(
  //"org.scalatest" %% "scalatest" % "2.1.7" % "test"
  "org.scalatest" %% "scalatest" % "2.2.0" % "test"
)

以下、順に。

テストコード。

    it("compare values") {
      val (a, b, c, d) = (1, 2, 3, 4)

      assert(a == b || c >= d)
    }

ScalaTest 2.1.7での結果。

[info] - compare values *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException was thrown. (ScalaTestSampleSpec.scala:13)

ScalaTest 2.2.0での結果。

[info] - compare values *** FAILED ***
[info]   1 did not equal 2, and 3 was not greater than or equal to 4 (ScalaTestSampleSpec.scala:13)

テストコード。

    it("List#exists#1") {
      val xs = List(1, 2, 3)

      assert(xs.exists(_ == 4))
    }

ScalaTest 2.1.7での結果。

[info] - List#exists#1 *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException was thrown. (ScalaTestSampleSpec.scala:19)

ScalaTest 2.2.0での結果。

[info] - List#exists#1 *** FAILED ***
[info]   List(1, 2, 3) did not contain 4 (ScalaTestSampleSpec.scala:19)

テストコード。

    it("string test") {
      assert("hello".startsWith("h") && "goodbye".endsWith("y"))
    }

ScalaTest 2.1.7での結果。

[info] - string test *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException was thrown. (ScalaTestSampleSpec.scala:23)

ScalaTest 2.2.0での結果。

[info] - string test *** FAILED ***
[info]   "hello" started with "h", but "goodbye" did not end with "y" (ScalaTestSampleSpec.scala:23)

テストコード。

    it("instanceOf") {
      val num = 1.0

      assert(num.isInstanceOf[Int])
    }

ScalaTest 2.1.7での結果。

[info] - instanceOf *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException was thrown. (ScalaTestSampleSpec.scala:29)

ScalaTest 2.2.0での結果。

[info] - instanceOf *** FAILED ***
[info]   1.0 was not instance of scala.Int (ScalaTestSampleSpec.scala:29)

テストコード。

    it("Some#isEmpty") {
      assert(Some(2).isEmpty)
    }

ScalaTest 2.1.7での結果。

[info] - Some#isEmpty *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException was thrown. (ScalaTestSampleSpec.scala:33)

ScalaTest 2.2.0での結果。

[info] - Some#isEmpty *** FAILED ***
[info]   Some(2) was not empty (ScalaTestSampleSpec.scala:33)

テストコード。

    it("None#isDefined") {
      assert(None.isDefined)
    }

ScalaTest 2.1.7での結果。

[info] - None#isDefined *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException was thrown. (ScalaTestSampleSpec.scala:37)

ScalaTest 2.2.0での結果。

[info] - None#isDefined *** FAILED ***
[info]   scala.None.isDefined was false (ScalaTestSampleSpec.scala:37)

テストコード。

    it("List#exists#2") {
      val xs = List(1, 2, 3)

      assert(xs.exists(i => i > 10))
    }

ScalaTest 2.1.7での結果。

[info] - List#exists#2 *** FAILED ***
[info]   org.scalatest.exceptions.TestFailedException was thrown. (ScalaTestSampleSpec.scala:43)

ScalaTest 2.2.0での結果。

[info] - List#exists#2 *** FAILED ***
[info]   xs.exists(((i: Int) => i.>(10))) was false (ScalaTestSampleSpec.scala:43)

確かにこれは、随分改善された気がします…。というか、元が…。

DiagrammedAssertions

さて、続いてDiagrammedAssertionsを試してみましょう。

そもそもDiagrammedAssertionsとは何かというところですが、特にGroovy界隈(Spock)で有名なPower-Assertができるようになったということらしいです。Groovyでは使ったことがありませんが…。

先ほどのテストコードのimport文に、以下を追加します。

import org.scalatest.DiagrammedAssertions._

変更内容、これだけです。

では、実行してみましょう!

> test

結果。

[info] ScalaTestSampleSpec:
[info] Assertions Improve spec
[info] - compare values *** FAILED ***
[info]   1 did not equal 2, and 3 was not greater than or equal to 4 (ScalaTestSampleSpec.scala:13)

〜以下、省略〜

あれ…先ほどの結果と変わりませんが…?Release Notesで書いてたことと、同じようにやったのですが…。

と、ここでちょっと考えて、objectをimportするのではなくて、traitをmix-inする方向に変えてみました。つまり、こんな感じのクラス定義になりました。

import org.scalatest.FunSpec
import org.scalatest.DiagrammedAssertions

//class ScalaTestSampleSpec extends FunSpec {
class ScalaTestSampleSpec extends FunSpec with DiagrammedAssertions {

もう1度、テスト実行!

> test

結果。

[info] ScalaTestSampleSpec:
[info] Assertions Improve spec
[info] - compare values *** FAILED ***
[info]   assert(a == b || c >= d)
[info]          | |  | |  | |  |
[info]          1 |  2 |  3 |  4
[info]            |    |    false
[info]            |    false
[info]            false (ScalaTestSampleSpec.scala:13)

〜以下、省略〜

やった!変わりました!まあ、Release NotesにはDiagrammedAssertionsトレイトって書いてますしね…。こうするのが正しいのかなぁ…。

では、結果をそれぞれ対比していってみましょう。

テストコード。

    it("compare values") {
      val (a, b, c, d) = (1, 2, 3, 4)

      assert(a == b || c >= d)
    }

ScalaTest 2.2.0での通常のassert。

[info] - compare values *** FAILED ***
[info]   1 did not equal 2, and 3 was not greater than or equal to 4

ScalaTest 2.2.0でのDiagrammedAssertions。

[info] - compare values *** FAILED ***
[info]   assert(a == b || c >= d)
[info]          | |  | |  | |  |
[info]          1 |  2 |  3 |  4
[info]            |    |    false
[info]            |    false
[info]            false (ScalaTestSampleSpec.scala:13)

テストコード。

    it("List#exists#1") {
      val xs = List(1, 2, 3)

      assert(xs.exists(_ == 4))
    }

ScalaTest 2.2.0での通常のassert。

[info] - List#exists#1 *** FAILED ***
[info]   List(1, 2, 3) did not contain 4 (ScalaTestSampleSpec.scala:19)

ScalaTest 2.2.0でのDiagrammedAssertions。

[info] - List#exists#1 *** FAILED ***
[info]   assert(xs.exists(_ == 4))
[info]          |  |
[info]          |  false
[info]          List(1, 2, 3) (ScalaTestSampleSpec.scala:19)

テストコード。

    it("string test") {
      assert("hello".startsWith("h") && "goodbye".endsWith("y"))
    }

ScalaTest 2.2.0での通常のassert。

[info] - string test *** FAILED ***
[info]   "hello" started with "h", but "goodbye" did not end with "y" (ScalaTestSampleSpec.scala:23)

ScalaTest 2.2.0でのDiagrammedAssertions。

[info] - string test *** FAILED ***
[info]   assert("hello".startsWith("h") && "goodbye".endsWith("y"))
[info]          |       |          |    |  |         |        |
[info]          "hello" true       "h"  |  "goodbye" false    "y"
[info]                                  false (ScalaTestSampleSpec.scala:23)

テストコード。

    it("instanceOf") {
      val num = 1.0

      assert(num.isInstanceOf[Int])
    }

ScalaTest 2.2.0での通常のassert。

[info] - instanceOf *** FAILED ***
[info]   1.0 was not instance of scala.Int (ScalaTestSampleSpec.scala:29)

ScalaTest 2.2.0でのDiagrammedAssertions。

[info] - instanceOf *** FAILED ***
[info]   assert(num.isInstanceOf[Int])
[info]          |   |
[info]          1.0 false (ScalaTestSampleSpec.scala:29)

テストコード。

    it("Some#isEmpty") {
      assert(Some(2).isEmpty)
    }

ScalaTest 2.2.0での通常のassert。

[info] - Some#isEmpty *** FAILED ***
[info]   Some(2) was not empty (ScalaTestSampleSpec.scala:33)

ScalaTest 2.2.0でのDiagrammedAssertions。

[info] - Some#isEmpty *** FAILED ***
[info]   assert(Some(2).isEmpty)
[info]          |    |  |
[info]          |    2  false
[info]          Some(2) (ScalaTestSampleSpec.scala:33)

テストコード。

    it("None#isDefined") {
      assert(None.isDefined)
    }

ScalaTest 2.2.0での通常のassert。

[info] - None#isDefined *** FAILED ***
[info]   scala.None.isDefined was false (ScalaTestSampleSpec.scala:37)

ScalaTest 2.2.0でのDiagrammedAssertions。

[info] - None#isDefined *** FAILED ***
[info]   assert(None.isDefined)
[info]          |    |
[info]          None false (ScalaTestSampleSpec.scala:37)

テストコード。

    it("List#exists#2") {
      val xs = List(1, 2, 3)

      assert(xs.exists(i => i > 10))
    }

ScalaTest 2.2.0での通常のassert。

[info] - List#exists#2 *** FAILED ***
[info]   xs.exists(((i: Int) => i.>(10))) was false (ScalaTestSampleSpec.scala:43)

ScalaTest 2.2.0でのDiagrammedAssertions。

[info] - List#exists#2 *** FAILED ***
[info]   assert(xs.exists(i => i > 10))
[info]          |  |
[info]          |  false
[info]          List(1, 2, 3) (ScalaTestSampleSpec.scala:43)

なるほどー。

オマケ

ちなみに、自分が普段使っているMatcherだとこういうコードになって
src/test/scala/org/littlewings/scalatest/ScalaTestSample2Spec.scala

src/test/scala/org/littlewings/scalatest/ScalaTestSample2Spec.scala 
package org.littlewings.scalatest

import org.scalatest.FunSpec
import org.scalatest.Inspectors._
import org.scalatest.Matchers._

class ScalaTestSample2Spec extends FunSpec {
  describe("Use Matchers spec") {
    it("compare values") {
      val (a, b, c, d) = (1, 2, 3, 4)

      (a == b || c >= d) should be (true)
    }

    it("List contains") {
      val xs = List(1, 2, 3)

      xs should contain (4)
    }

    it("string test") {
      "hello" should startWith ("h")
      "goodbye" should endWith ("y")
    }

    it("instanceOf") {
      val num = 1.0

      num should be (a [Int])
    }

    it("Some#isEmpty") {
      Some(2) should be (empty)
    }

    it("None#isDefined") {
      None should not be (empty)
    }

    it("List#exists") {
      val xs = List(1, 2, 3)

      all (xs) should be > 10
    }
  }
}

こういう結果表示になります。

[info] ScalaTestSample2Spec:
[info] Use Matchers spec
[info] - compare values *** FAILED ***
[info]   false was not true (ScalaTestSample2Spec.scala:12)
[info] - List contains *** FAILED ***
[info]   List(1, 2, 3) did not contain element 4 (ScalaTestSample2Spec.scala:18)
[info] - string test *** FAILED ***
[info]   "goodbye" did not end with substring "y" (ScalaTestSample2Spec.scala:23)
[info] - instanceOf *** FAILED ***
[info]   1.0 was not an instance of int, but an instance of java.lang.Double (ScalaTestSample2Spec.scala:29)
[info] - Some#isEmpty *** FAILED ***
[info]   Some(2) was not empty (ScalaTestSample2Spec.scala:33)
[info] - None#isDefined *** FAILED ***
[info]   None was empty (ScalaTestSample2Spec.scala:37)
[info] - List#exists *** FAILED ***
[info]   'all' inspection failed, because: 
[info]     at index 0, 1 was not greater than 10 (ScalaTestSample2Spec.scala:40) 
[info]   in List(1, 2, 3) (ScalaTestSample2Spec.scala:43)

さて、どっちがいいでしょうかね?