読者です 読者をやめる 読者になる 読者になる

倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

ScalaTest も『JUnit 実践入門』もまとめて相手してやんよ (6) : Matcher いろいろ~コレクション編~

Scala テスト

前回に続き、今回も ScalaTest に定義済みの Matcher を見ていきます(目次)。 今回扱うのはコレクション関連の Matcher です。

Scala のコレクションは Java のコレクションとは別の型ですが、ScalaTest のコレクション関連の Matcher はどちらも同じように扱うことができます。 また JUnit ではコレクションと配列の Matcher は別に定義されてますが、ScalaTest では統一的に扱うことができます。 さらにマップも(Scala の Map か Java の Map かに関わらず)コレクションとして扱えます。 その他、String や Option なども同じように検証できます(Option についてはコンテナ (container) としてのみ可能。 以下参照)。

記事の内容

参考

  • 『JUnit 実戦入門』 4.3 Matcher API の使用
  • ScalaTest User Guide 「Using matchers
  • ScalaTest Scaladoc 「Matchers

Container

まずは何らかのオブジェクトを含むことができる Container に関する Matcher。 Container には

  • Scala のコレクション
  • 配列
  • Java のコレクション
  • Java のマップ
  • 文字列 String (文字のコンテナとして)
  • scala.Option

などがあります。 共通して使える Matcher は以下の通り(「SUT」は検証対象のオブジェクトです):

  • 空であることを検証 empty
    • SUT should be (empty)
    • SUT shouldBe empty
  • 要素として含むか検証
    • SUT should contain (オブジェクト)
  • 指定されたオブジェクト群の1つを要素として含むか検証
    • SUT should contain oneOf (オブジェクト1, オブジェクト2, ... )
  • 指定されたオブジェクト群の1つも要素として含まないか検証
    • SUT should contain noneOf (オブジェクト1, オブジェクト2, ... )

oneOf は指定した要素のうち1つだけを含む場合に検証が通ります。 また、oneOf / noneOf の後のオブジェクトの列挙部分では、等価なオブジェクトを複数指定すると例外が投げられます(「contain oneOf (1, 1, 2)」など)。

あるオブジェクトを含むかどうかを評価するためには要素の等価性を指定する必要があります。 もちろん指定しなければ通常の == による評価が行われますが、これを変更することもできます(例えば文字列を大文字小文字の区別なく等しいかどうかを評価するなど)。 これはカスタムマッチャーを作成するときにやる予定(大して難しいわけではないですが)。

ではサンプルコード。

class ContainerMatcherSpec extends FlatSpec with Matchers{

  "should be empty / shouldBe empty" should "空である" in {
    // 空のオブジェクトあれこれ
    None should be (empty)  // Option
    "" should be (empty)  // String
    Nil shouldBe empty  // List
    List() shouldBe empty  // List
  }

  // JUnit : hasItem
  "contain" should "指定された要素を持つ" in {
    "ScalaTest" should contain ('T')
    // "ScalaTest" should contain ("T")  // 失敗:文字列の要素は文字
    Some(1) should contain (1)
    List(0, 1, 2) should contain (1)
    Array(0, 1, 2) should contain (1)
    Map("one" -> 1, "two" -> 2, "three" -> 3) should contain ("one" -> 1)
      // Scala の Map はタプルのコレクション(Java の Map は後で)

    import java.util.Arrays.asList
    asList(0, 1, 2) should contain (1)  // Java のコレクションも OK
  }

  "contain oneOf/noneOf" should "指定された要素のうち1つだけ含む/1つも含まない" in {
    // Option
    Some(1) should contain oneOf (1, 2, 3)
    Some(1) should contain noneOf (4, 5, 6)

    // Some(1) should contain oneOf (1, 2, 3, 3)  // ダブった値の指定は例外が投げられる

    // List
    List(0, 1, 2) should contain oneOf (2, 3, 4)
    List(0, 1, 2) should not contain oneOf (5, 6, 7)
    List(0, 1, 2) should not contain oneOf (1, 2, 3)  // 含んでいいのは1つだけ
  }
}

Aggregation

Aggregation は Container のうち、特に複数の要素を含み得るものです。 多くとも1つしか要素を含み得ない Option は、Container ではあるが Aggregation ではない、というのがわかりやすいでしょうか。 以下に挙げる Aggregation の Matcher は Option にも使えそうですが、使わなくても Container に対する Matcher で同じことが行える*1ので使う必要がありません(実際にはコンパイルが通らなくて使えませんが)。

  • サイズを検証 have size
    • SUT should have size 数値
  • 指定された要素群の少なくとも/多くとも1つを含む atLeastOneOf / atMostOneOf
    • SUT should contain atLeastOneOf (オブジェクト1, オブジェクト2, ... )
    • SUT should contain atMostOneOf (オブジェクト1, オブジェクト2, ... )
  • 指定された要素群を全て含む allOf
    • SUT should contain allOf (オブジェクト1, オブジェクト2, ... )
  • 指定された要素群のみを含む only
    • SUT should contain only (オブジェクト1, オブジェクト2, ... )
  • 指定されたコレクションと同じ要素を持つ theSameElementsAs
    • SUT should contain theSameElementsAs コレクション

have size の後の数値には丸括弧 () は必要ありません。

atLeastOneOf, atMostOneOf, allOf, only の後のオブジェクト群の指定は、Container の oneOf / noneOf と同じく等価なオブジェクトを複数指定することはできません。 一方、theSameElementsAs の後にはコレクションを指定するので、等価なオブジェクトが複数指定されていても問題ありません。

contain only は、SUT のコレクションに含まれている要素が全て指定されたオブジェクト群に含まれていなければいけませんが、含まれている回数は問われません。 サンプルコード参照。

ではサンプルコード:

class AggregationMatcherSpec extends FlatSpec with Matchers{

  // Java の Map。 後で使う
  val jmap: java.util.Map[String, Int] = new java.util.HashMap()
  jmap.put("one", 1)
  jmap.put("two" ,2)
  jmap.put("three", 3)

  "have size" should "サイズが指定された値である" in {
    "ScalaTest" should have size 9
    Seq(1, 1, 2, 3, 5) should have size 5
    Set(1, 1, 2, 3, 5) should have size 4
    Map("one" -> 1, "two" -> 2, "three" -> 3) should have size 3

    // Java のコレクション・マップも OK
    import java.util.Arrays.asList
    asList(1, 2, 3) should have size 3  // Java の List
    jmap should have size 3  // Java の Map
  }

  "contain atLeastOneOf/atMostOneOf" should "指定された要素のうち少なくとも/多くとも1つ含む" in {
    List(0, 1, 2) should contain atLeastOneOf (1, 2, 3)  // 共通要素2個○
    List(0, 1, 2) should contain atLeastOneOf (2, 3, 4)  // 共通要素1個○
    List(0, 1, 2) should not contain atLeastOneOf (3, 4, 5)  // 共通要素なし×

    List(0, 1, 2) should contain atMostOneOf (3, 4, 5)  // 共通要素なし○
    List(0, 1, 2) should contain atMostOneOf (2, 3, 4)  // 共通要素1個○
    List(0, 1, 2) should not contain atMostOneOf (1, 2, 3)  // 共通要素2個×
  }

  "contain allOf" should "指定された要素をすべて含む" in {
    List(0, 1, 2) should contain allOf (1, 2)
    List(0, 1, 2) should not contain allOf (1, 2, 3)  // 含まない要素がある×
  }

  "contain only" should "指定された要素のみを含む" in {
    List(1, 1, 2, 3) should contain only (1, 2, 3)  // 含まれている回数は問わない

    List(0, 1, 2, 3) should not contain only (1, 2, 3)  // 要素0は指定されてない×
    List(1, 1, 2, 2) should not contain only (1, 2, 3)  // 要素3が含まれてない×
  }

  "contain theSameElementsAs" should "指定されたコレクションと同じ要素を持つ" in {
    // theSameElementsAs の後には Scala のコレクションを指定する必要あり
    // ここでは Seq
    List(1, 2, 3) should contain theSameElementsAs Seq(3, 1, 2)
    List(1, 2, 3) should not contain theSameElementsAs (Seq(1, 1, 2, 3))
  }
}

英語の意味的に(というかイメージとして) at Least/Most がスッと飲み込めないと、英文風に書いてるテストコードが苦痛に感じるかも(笑)

Sequence, Sortable

Sequence はアグリゲーションのうち、要素を含む順序が定められたものです。 まぁ、Seq, Array, String などをイメージしておけば充分かと。 Sequence に関する Matcher は、Aggregation に関する Matcher に順序 (order) を考慮したものがほとんどです。

  • 長さを検証 have length
    • SUT should be have length 数値
  • 指定された要素群を同じ順序で含む inOrder
    • SUT should contain inOrder (オブジェクト1, オブジェクト2, ... )
  • 指定された要素群のみを同じ順序で含む inOrderOnly
    • SUT should contain inOrderOnly (オブジェクト1, オブジェクト2, ... )
  • 指定されたコレクションと同じ要素を同じ順序で持つ theSameElementsInOrderAs
    • SUT should contain theSameElementsInOrderAs シーケンス

要素の個数に関して、Java では特に size と length の使い分けはなく、Groovy では全て size に統一されていますが、Scala では順序がないものが size、順序のあるものが length という使い分けになっています。 したがって、Set や Map のようなコレクションに length は使えません。

Sortable は含まれている要素自体に順序関係がある(定義できる) Sequence です。 順序は昇順(小さい順)。

  • ソートされている sorted
    • SUT should be (sorted)
    • SUT shouldBe sorted

contain でのオブジェクトの等値性(同値関係)と同じく、ソートに用いる順序関係も外部から指定できると思いますが、ここではスルー。 【追記】等価性の評価と比べ順序関係のカスタマイズは API としてサポートされていないようなので、あまり簡単に順序関係を指定できないようです。

import java.util.Arrays.asList

class SequenceMatcherSpec extends FlatSpec with Matchers{

  "have length" should "長さが指定された値である" in {
    Seq(1, 1, 2, 3, 5) should have length 5
    "ScalaTest" should have length 9  // 文字列は文字のシーケンス

    // Set には length は使えないヨ
    "Set(1, 1, 2, 3, 5) should have length 4" shouldNot compile
  }

  "contain inOrder/inOrderOnly" should "指定された要素を/要素だけを順に含む" in {
    // inOrder
    List(1, 2, 3, 3) should contain inOrder (1, 2, 3)  // 回数は気にしなくてOK
    List(1, 2, 4, 3) should contain inOrder (1, 2, 3)  // 他の要素(4)は気にしなくてOK
    List(1, 2, 3, 1) should not contain inOrder (1, 2, 3)

    // inOrderOnly
    List(1, 2, 3, 3) should contain inOrderOnly (1, 2, 3)
    List(1, 2, 3, 4) should not contain inOrderOnly (1, 2, 3)  // こちらは他の要素が入ってるとダメ
  }

  "contain theSameElementsInOrderAs" should "指定されたを要素を同じ順番で持つ" in {
    List(1, 2, 3, 3) should contain theSameElementsInOrderAs Seq(1, 2, 3, 3)
    List(1, 2, 3, 4) should not contain theSameElementsInOrderAs (Seq(1, 2, 3))
    List(1, 2, 3, 3) shouldNot contain theSameElementsInOrderAs Seq(1, 2, 3)

    // java.util.List と Scala の List も比較可
    asList(1, 2, 3, 3) should contain theSameElementsInOrderAs List(1, 2, 3, 3)
    // List(1, 2, 3) should contain theSameElementsInOrderAs asList(1, 2, 3)  逆はダメ
  }

  "be (sorted)" should "要素がソートされている" in {
    List(1, 2, 3, 4) should be (sorted)
    List(1, 2, 3, 3) shouldBe sorted
    List(1, 2, 3, 1) should not be sorted

    // 配列や java.util.List も OK
    Array(1, 2, 3, 3) shouldBe sorted
    asList(1, 2, 3, 3) shouldBe sorted
  }
}

マップ Map

Java の Map は Collection を継承していないので Collection と別モノとして扱う必要がありますが、Scala の Map はタプルのコレクション(というか Iterable)なので上記の Matcher のうち Container と Aggregation に関するものは同じように使えます。 他にキーや値に関する Matcher も使えます。

さらに ScalaTest では Java の Map もコレクションのように扱えるようにしてくれてあります。 ただし、この場合の要素はタプルではなく org.scalatest.Entry オブジェクトなので、この型をインポートしておく必要があります。 キーや値の検証は Scala の Map と同じように行えます。

  • キーを含む contain key
    • SUT should contain key キー
  • 値を含む contain value
    • SUT should contain value
  • エントリーを含む contain
    • SUT should contain (キー -> 値)
  • エントリーを含む(Java の Map) contain Entry
    • SUT should contain Entry(キー, 値)

ではサンプルコード:

class MapMatcherSpec extends FlatSpec with Matchers{

  "contain key/value/( -> )" should "指定されたキー/値/エントリーを含む" in {
    val map = Map("one" -> 1, "two" -> 2, "three" -> 3)

    map should contain key "one"
    map should contain value 2
    map should contain ("three" -> 3)

    map should not contain ("four" -> 4)
    map shouldNot contain ("five" -> 5)
  }

  "contain key/value/Entry (for Java map)" should "指定されたキー/値/エントリーを含む" in {
    val map = new java.util.HashMap[String, Int]
    map.put("one", 1)
    map.put("two", 2)
    map.put("three", 3)

    map should contain key "one"
    map should contain value 2

    import org.scalatest.Entry
    map should contain (Entry("three", 3))
    map should not contain Entry("four", 4)
  }
}

Inspector

Inspector はこれまで出てきた Matcher とは少し違って(というか Matcher ではなくて)、コレクションである SUT を修飾するように書きます。 例えば、list をリストオブジェクトとして「all(list) should ...」と書くことで、list の要素全てについて ... 部分の Matcher を満たす、という検証を行います。 ... の部分には任意の Matcher を書けます。

  • すべての要素について all, every
    • all(SUT) should ...
    • every(SUT) should ...
  • すべての要素について成り立たない no
    • no(SUT) should ...
  • 指定した個数の要素について exactly
    • exactly(回数, SUT) should ...
  • 指定した個数以上/以下の要素について atLeaset/atMost
    • atLeast(回数, SUT) should ...
    • atMost(回数, SUT) should ...
  • 指定した範囲の個数の要素について between
    • between(下限, 上限, SUT) should ...

allevery はどちらも全ての要素について条件が満たされることを要求しますが、失敗時に出力されるメッセージが異なります。 all では最初に条件が満たされなかった要素が出力されるのに対して、every では条件を満たされなかった要素全てを列挙します。

個人的にちょっと気になったのが、「all(SUT) should not be (1)」のように all を not で否定したとき、(高校英語でやるように)英文的には部分否定の意味になるけど実際にはどうなのかな?というところ。 まぁ、プログラム的に考えて完全否定になるだろうなぁと予想できるし、実際そのように動作します。 各要素について should 以下の検証が行われ、検証が通った要素の個数が指定した回数に一致するか検証されるってイメージですね。 ちなみに all の場合は素直に no を使っておく方が無難でしょう。

ではサンプルコード。

class InspectorMatcherSpec extends FlatSpec with Matchers{

  "all" should "すべての要素が条件を満たす" in {
    val list = List(1, 3, 5, 7, 9)
    all(list) should be > 0
    all(list) shouldNot be > 10  // 完全否定
//    all(list) should be <= 10 と同じ
  }

  "every" should "allと同じ(ただし失敗時にすべての要素を列挙する)" in {
    val list = List(1, 3, 5, 7, 9)
    every(list) should be > 0
  }

  "no" should "すべての要素が条件を満たさない" in {
    val list = List(1, 3, 5, 7, 9)
    no(list) should be < 0
  }

  "exactly" should "指定された個数の要素だけ条件を満たす" in {
    val list = List(1, 3, 5, 7, 9)
    exactly(3, list) should be > 4
    exactly(2, list) shouldNot be > 4
    //    exactly(2, list) should be <= 4 と同じ
  }

  "atLeast" should "少なくともいくつかの要素が条件を満たす" in {
    val list = List(1, 3, 5, 7, 9)
    atLeast(2, list) should be > 6
  }

  "atMost" should "多くともいくつかの要素しか条件を満たさない" in {
    val list = List(1, 3, 5, 7, 9)
    atMost(3, list) should be > 4
  }

  "between" should "条件に合う要素の個数が指定された範囲内である" in {
    val list = List(1, 3, 5, 7, 9)
    between(2, 3, list) should be > 6
  }
}

その他のユーティリティ

今回の最後は、コレクションや Option に関連するいくつかのユーティリティ。

  • 定義されている defined
    • SUT should be (defined)
  • 指定された値で定義されている definedAt
    • SUT should be definedAt オブジェクト
  • Option の値を取り出して検証する value
    • SUT.value should ...
  • 要素を1つしか持たないコレクションの要素を取り出して検証する loneElement
    • SUT.loneElement should ...
  • Iterator の要素を検証する toStream
    • SUT.toStream should ...

defined の「定義されている」というのは抽象的ですが、Boolean 値の isDefined プロパティが取得できるという程度の意味です*2 definedAt も同様で、isDefinedAt メソッドがあれば検証に使えます。

Option に対する value とコレクションに対する loneElement は要素を簡単に取り出すためのユーティリティです。 Option が None だったり要素が1つでなかったりした場合、TestFailedException が投げられます。 これらのメソッドを使うためには、それぞれ

  • org.scalatest.OptionValues._,
  • org.scalatest.LoneElement._

をインポートしておく必要があります。

toStream は、Iterator オブジェクトに対する Matcher がないので Stream に変換してから検証しましょう、というだけのメソッド。

ではサンプルコード:

import org.scalatest.exceptions.TestFailedException
import org.scalatest.{FlatSpec, Matchers}

class ContainerSyntaxMatcherSpec extends FlatSpec with Matchers{

  "defined" should "定義されている" in {
    Some(1) should be (defined)
    None should not be defined
  }

  "definedAt" should "指定した値で定義されている" in {
    List(1, 2) should be definedAt 1
    List(1, 2) should not be definedAt (3)

    val pf:PartialFunction[Int,Int] = { case 1 => 1 }
    pf should be definedAt 1
  }

  "Option.value" should "Optionの値を取り出して検証を行う" in {
    import org.scalatest.OptionValues._

    Some(1).value should be > 0
    a [TestFailedException] should be thrownBy{ None.value }
      // None に対して呼び出すと TestFailedException が投げられる
  }

  "loneElement" should "コレクションの唯一の値を取り出して検証を行う" in {
    import org.scalatest.LoneElement._

    Set(1).loneElement should be > 0
    a [TestFailedException] should be thrownBy{ Set.empty[Int].loneElement }
    a [TestFailedException] should be thrownBy{ Set(1, 2).loneElement }
      // 要素が1つでないと TestFailedException が投げられる
  }

  "toStream" should "IteratorをStreamに変換して検証を行う" in {
    val ite = List(1, 2, 3).iterator

    ite.toStream should contain (2)
  }
}

ScalaTest のコレクションサポートは JUnit (というか hamcrest-library)のものに比べて統一的で分かりやすい気がしますね。 しかも Java のコレクションや Map すらも同じように扱えるなんて。 まぁ、ScalaTest の力でもあり、Scala の力でもあるんでしょうけど。 さて、次回で Matcher 最終回の予定。 残りの Matcher をまとめて相手してやんよ。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

Scala関数型デザイン&プログラミング―Scalazコントリビューターによる関数型徹底ガイド

Scala関数型デザイン&プログラミング―Scalazコントリビューターによる関数型徹底ガイド

*1:例えば Option の size は0か1ですが、empty を使えばどちらかが分かる、atLeastOneOf は oneOf と同じ、atMostOneOf は常にtrue、など。

*2:本当はもうちょっと高尚で、org.scalatest.enablers.Definition[T] 型の暗黙のパラメータがあればこの検証ができます。