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

倭マン's BLOG

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

ScalaTest も『JUnit 実践入門』もまとめて相手してやんよ (7) : Matcher いろいろ~論理演算・その他編~

Scala テスト

さて、今回で ScalaTest の基本的な Matcher は完結(目次)。 扱うのは他の Matcher を組み合わせる論理演算と、ちょっとメタプログラミングっぽい(※個人の感想です) Matcher たち。

記事の内容

参考

【追記】
パターンマッチングの箇所に matchPattern を追記しました。

論理演算

まずは他の Matcher を論理演算で組み合わせる Matcher。 スタンダードな not, and, or があります。

  • 否定 not
    • SUT should not マッチャー
  • 論理積/論理和 and/or
    • SUT should (マッチャー1 and マッチャー2)
    • SUT should (マッチャー1 or マッチャー2)

and, or では should の後に丸括弧 () が必要なことに注意。 マッチャー1、マッチャー2 にあたる部分には、他の Matcher を使う際に should の後に続ける部分をそのまま書いていきます。 また、and, or は3つ以上つなげることもできます。

and, or で注意が必要なのは

  • ショートサーキットではない。 通常の && や || と異なり、連結されている全ての Matcher を評価します。 ただし、失敗時のメッセージはあたかもショートサーキットのように書かれているので、検証の失敗部分を特定するのが難しくなったりしません。
  • and と or の優先順位は等しく、左から右へ評価されます。 つまり、「a || b && c」は「a || (b && c)」と同じなのに対し、「a or b and c」は「(a or b) and c」と同じ振る舞いをします。

ではサンプルコード。

class LogicalMatcherSpec extends FlatSpec with Matchers{

  "not" should "否定する" in {
    "ScalaTest" should startWith ("Scala")
    "ScalaTest" should not startWith "Java"
  }

  "and" should "かつ(論理積)" in {
    val map = Map("one" -> 1, "two" -> 2, "three" -> 3)

    map should (have size 3 and contain key "one")
  }

  "or" should "または(論理和)" in {
    val map = Map("one" -> 1, "two" -> 2, "three" -> 3)

    map should (have size 3 or contain ("four" -> 4))
  }
}

ちなみに、「should not」は「shouldNot」と大体同じですが、Scala コードとしては結構違います。 次の2つのコード

    "ScalaTest" shouldNot startWith ("Java")
    "ScalaTest" should not startWith "Java"

を暗黙の型変換やメソッドのピリオド・括弧を省略せずに書くと

    convertToAnyShouldWrapper("ScalaTest").shouldNot(startWith.apply("Java"))
    convertToAnyShouldWrapper("ScalaTest").should(not).startWith("Java")

shouldNot の場合の startWith は Matchers トレイトに定義されたフィールドなのに対して、should not の場合の startWith は convertToAnyShouldWrapper メソッドで返されたオブジェクト(AnyShouldWrapper 型)のメソッドとなっています。 このため、should (shouldNot) のときに必要だった丸括弧 () が、should not の場合に必要なくなったりします。 まぁ、IDE を使ってるとあんまり気にする必要はないんですが、ScalaTest があの手この手で英文っぽく書けるように努力してるのが垣間見えて興味深いです。

JavaBeans

次は JavaBeans*1 のプロパティを検証する Matcher。 正確には JavaBeans のプロパティと言っていいのか分かりませんが、とにかくよくある getter (isXxxx 形式のものも含む)メソッドやパブリックなフィールドなどで取得できる、オブジェクトの属性値を検証します。

  • Boolean 値のプロパティが true であることを検証 be a/an
    • SUT should be a 'プロパティ名
  • プロパティが指定した値を持つことを検証 have (...)
    • SUT should have ('プロパティ名1 (プロパティ値1), 'プロパティ名2 (プロパティ値), ... )

be a/an は Boolean 値のプロパティ (isXxxx) の値が true であることを検証します。 a/an の後にはシンボルを指定するためにクォート (') に続けてプロパティ名を書きます。

Boolean 値以外のプロパティも検証したい場合は hava を使います(Boolean 値のプロパティももちろん OK)。 have の後にプロパティ名とプロパティ値を列挙していくのですが、have の後とプロパティ値に丸括弧 () を付ける必要があることに注意。 検証したいプロパティが1つでも have の後の丸括弧は省略できません。

ではサンプルコード。

class JavaBeansMatcherSpec extends FlatSpec with Matchers with Inside{

  "be a/an '《symbol》" should "Boolean値のプロパティがtrueである" in {
    val f = File.createTempFile("prop-", ".txt")
    f should be a 'file  // File#isFile() メソッドが true を返すことを検証
    f shouldNot be a 'directory

    val dir = File.createTempDirectory("prop-")
    dir should be a 'directory
    dir shouldNot be a 'hidden
  }

  case class Person(name:String, age:Int, sex:String)

  "have" should "プロパティが指定された値を持つ" in {
    val me = Person("waman", 100, "male")

    me should have (
      'name ("waman"),
      'age  (100),
      'sex  ("male")
    )
  }
}

パターンマッチング

matchPattern によって検証対象のオブジェクトが指定したパターンにマッチするかを検証することができます。 また、Inside トレイトをミックスインすれば、inside によってパターンマッチングで目的のオブジェクト抽出して検証することもできます。 inside 自体は Matcher と大して関係ありませんが(パターンにマッチしないときにテスト失敗の例外を投げるとかはあるかもしれません)。

  • パターンマッチで抽出したものを検証 matchPattern, inside
    • SUT should matchPattern { case パターン => }
    • inside(SUT){ case パターン => 検証コード }

matchPattern に渡す部分関数に本体(=> の後)は要りません。

サンプルコード見た方が分かりやすいかな。

class PatternMatcherSpec extends FlatSpec with Matchers with Inside{

  case class Person(name:String, age:Int, sex:String)

  "matchPattern" should "パターンにマッチしていることを検証する" in {
    val me = Person("waman", 100, "male")

    me should matchPattern { case Person(_, 100, _) => }
  }

  "inside" should "パターンマッチで抽出したものを検証する" in {
    val me = Person("waman", 100, "male")

    inside(me){ case Person(_, age, _) =>
      age should be >= 20
    }
  }
}

matchPattern を使うだけなら Inside トレイトをミックスインする必要はありません。

コード断片

最後は Scala コードとしての文字列がコンパイルできることを検証する Matcher。 「ScalaTest も『JUnit 実践入門』もまとめて相手してやんよ (2) : ScalaTest 特化機能1」のコード断片のコンパイルの箇所で見た assertCompiles, assertTypeCheck を DSL にした Matcher です。

  • コード断片がコンパイル可能/不可能 compile
    • SUT should compile
    • SUT shouldNot compile
  • コード断片がコンパイル時に型チェックを通らない
    • SUT shouldNot typeCheck

compile, typeCheck では「SUT should not (compile)」などとは書けないようです。 否定の場合は shouldNot を使いましょう(そのうち使えるようになるかもしれないけど)。

import org.scalactic.TypeCheckedTripleEquals  // compile, typeCheck に必須というわけではない

class CodeSnippetMatcherSpec extends FlatSpec with Matchers with TypeCheckedTripleEquals{

  "compile" should "コード断片がコンパイルできる" in {
    "var i = 1 ; i = 2" should compile
    "val i = 1 ; i = 2" shouldNot compile  // val では変数への値の再代入不可。
  }

  "shouldNot typeCheck" should "コード断片がコンパイル時に型チェックを通らない" in {
    "val s : String = 10" shouldNot typeCheck
    "10 === 10.0" shouldNot typeCheck
  }
}

TypeCheckedTripleEquals は「===」の両辺の型チェックを行う(型が異なればコンパイル・エラーになる)ようにするためのトレイト。 詳しくは「ScalaTest も『JUnit 実践入門』もまとめて相手してやんよ (2) : ScalaTest 特化機能1」のTripleEquals 「===」を参照。

さて、これで ScalaTest に定義済みの Matcher は一通り見られたので、次はカスタムマッチャー(独自マッチャー)の作り方を見ていく予定。

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

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

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

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

*1:JavaBeans の定義には引数無しのコンストラクタが必要という条件などもありますが、この Matcher では気にする必要はありません。