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

倭マン's BLOG

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

ScalaTest も『JUnit 実践入門』もまとめて相手してやんよ (5) : Matcher いろいろ~各種クラス編~

Scala テスト

今回から何回かにわたって ScalaTest の Matcher による検証方法を見ていきます(目次)。 JUnit の assertThat に対応するものですが、Scala の括弧省略や暗黙の型変換によって通常の英文のように書くことができます。 日本人にとってはそんなにありがたくないかも知れませんが、定型文なので恐るるに足りません。 一部に省略できない括弧がありますが、IDE を使えば警告なりエラーなりを出してくれるので、こちらもそんなに問題にはならないと思います。

ScalaTest の Matcher はテストに関する DSL、通常の英文として読めるコードとなってますが、逆に Scala コードとしてどのオブジェクトに対してどのメソッドが何を引数にして呼び出しているのか、パッと見では分かりにくくなってます。 もし自分で DSL を書きたいときがあったら役立つかと思って ScalaTest の DSL が Scala コードとしてどのように構築されてるのかの説明も書こうと思ったんですが、ちょっと無駄に細かくなりそうだったのでこのシリーズではスルーすることに。 別途、記事を書く・・・かも。

記事の内容

参考

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

【追記】

  • 例外の have ('message ("文字列")) の項を追加しました。
  • 文字列で after being を用いて等価性を別途指定する方法を追加しました。

基本的な使い方

ScalaTest の Matcher を使用するには、まず準備として org.scalatest.Matchers トレイトをテストクラスにミックスインします:

import org.scalatest.{FlatSpec, Matchers}

class MySpec extends FlatSpec with Matchers{
   ...
}

準備はこれだけ。 もちろんテストスタイルは FlatSpec でなくても構いません。

で、テストコード中で Matcher を使うには、任意のオブジェクトに対して should メソッドを呼び出します。 暗黙の型変換によって should メソッドが定義されたオブジェクトに変換されるので心配不要*1。 この should メソッドに Matcher オブジェクトを渡せば検証が行われます。 カスタムマッチャー(独自マッチャー)の作り方は後ほどやることにして、今回(から当面の間)は定義済みの Matcher を見ていきます。

should メソッド以外にも

  • shouldBe
  • shouldEqual
  • shouldNot

が使えます。 それぞれ "should be", "should equal", "should not" と同じですが、後に続く部分の値に丸括弧 () を付けなくてよくなる場合があるので、括弧付けたくない方はどうぞ。

以下の Matcher の列挙部分での「SUT」は検証対象のオブジェクトを指します(System Under Test)。

Any

まずはどんな型のオブジェクトに対しても使える検証。 Scala の Any は Java でいう Object ですね(念のため)。 Java の Object#equals による検証に対応する == 演算子(メソッド)による等価性の検証や Java の == 演算子による検証に対応する AnyRef.eq メソッドによる同一参照の検証などの使い分けが必要。

  • オブジェクトの等価性を検証 be, equal, ===, shouldBe, shouldEqual
    • SUT should be (オブジェクト)
    • SUT should equal (オブジェクト)
    • SUT should === (オブジェクト)
    • SUT shouldBe オブジェクト
    • SUT shouldEqual オブジェクト
  • 同一参照を検証 theSameInstanceAs
    • SUT should be theSameInstanceAs オブジェクト*2
  • null 値検証
    • SUT should be (null)
    • SUT shouldBe null
  • 型検証
    • SUT should be (a [型])
    • SUT shouldBe a [型]

等価性の検証方法には5種類の書き方がありますが、お好みに合わせて。 等価性の検証に限らず、ほとんどの場合 should を使う際には期待値のオブジェクトに丸括弧 () を付ける必要があります。 shouldBeshouldEqual を使う場合はこの括弧は必要ありません。 「===」は TypeCheckedTripleEquals トレイトをミックスインしておけば型チェックも行ってくれます*3。 また、「equal」は独自の等価性を用いてオブジェクトを比較することができます。 文字列で大文字小文字を区別せずに比較する、トリムするなどは後述の「文字列 String」参照。 新たに等価性を定義する方法はカスタムマッチャーを作成する方法の記事で書く予定。

null 値の検証をする場合は、普通に期待値として null 値を用いれば OK (括弧は必要ですが)。 JUnit の nullValue / notNullValue のような別の Matcher は必要ありません。

型の検証には a を使います。 a の後には角括弧 [ ] で型を指定することに注意。 期待値の型が型パラメータを持つ場合は、型パラメータに具体的な型を指定するのではなく存在型(例えば List[_] など)にしましょう。 ちなみに、a と同じ動作をする an というのもあるので、必要ならこちらを使いましょう。

ではこれらの Matcher を使ったサンプルコード。 1つのテストケースの中で独立な検証をいくつも行っていますが、よい子のみなさんは真似しないように。

class AnyMatcherSpec extends FlatSpec with Matchers{

  // Java : Object#equals
  // JUnit : is
  // Scala : Any.==
  "equal" should "等価なオブジェクトまたは値である" in {
    "ScalaTest" should be ("ScalaTest")
    "ScalaTest" should equal ("ScalaTest")
    "ScalaTest" should === ("ScalaTest")

    "ScalaTest" shouldBe "ScalaTest"
    "ScalaTest" shouldEqual "ScalaTest"
  }

  // Java : ==
  // JUnit : sameInstance
  // Scala : AnyRef.eq
  "theSameInstanceAs" should "同一のインスタンスである" in {
    val s = "ScalaTest"
    val t = s
    s should be theSameInstanceAs t

    "ScalaTest" should not be theSameInstanceAs (new String("ScalaTest"))
  }

  // Java : == null
  // JUnit : nullValue() / notNullValue()
  // Scala : == null
  "be (null) / not be (null)" should "null値である" in {
    val actual:String = null
    actual should be (null)
    actual shouldBe null

    "null" should not be null
  }

  // Java : instanceof
  // JUnit : instanceOf
  // Scala : isInstanceOf
  "be a" should "指定したクラスのインスタンスである" in {
    "ScalaTest" should be (a [Serializable])
    "ScalaTest" shouldBe a [Serializable]

    "ScalaTest" should not be a [List[_]]
  }
}

文字列 String

次は文字列 String オブジェクトに関する Matcher。 単に具体的な文字列と等しいことを検証するだけなら、上記の等価性の検証(should be など)で事足りますが、文字列特有の比較を行う場合のために、いくつか文字列に特化した検証方法が定義されています。 大きく分けて

  • 文字列を適当に変形して比較する
  • 文字列の一部もしくは全部が正規表現にマッチする

という方法があります。

文字列を適当に変形して比較する
「should equal」ではオブジェクトの等価性の評価を Any.== によって行いますが、これを独自の等価性を使うように変更することもできます。 さらに、文字列の場合には、大文字小文字を区別せず比較するとかトリム(前後の空白文字を削除)して比較するというのはしばしば必要になるので、ある程度簡単に行えるようにしてくれています。

準備としては org.scalactic.StringNormalizations トレイト(パッケージ名注意。 scalatest ではない)をミックスインするだけです*4。 使い方は期待値のオブジェクトの後に「(after being ...)」を付けます。 定義済みの等価性は以下の3つ:

  • 文字列を大文字小文字の区別なく比較する lowerCased, upperCased
    • SUT should equal ("文字列") (after being lowerCased)
    • SUT should equal ("文字列") (after being upperCased)
  • 文字列をトリム(前後の空白文字を削除)して比較する
    • SUT should equal ("文字列") (after being trimmed)

大文字小文字の変換やトリムは、検証対象オブジェクト (SUT)、期待値のどちらの文字列に対しても行われます。 upperCased と lowerCased はどちらを使っても同じ検証結果になると思いますが、after being caseIgnored とかじゃダメだったんでしょうかね。 ちなみに、lowerCased と trimmed を一緒に使いたい場合は「after being lowerCased and trimmed」のように and でつなぎます。

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

import org.scalactic.StringNormalizations

class StringEqualityMatcherSpec extends FlatSpec with Matchers with StringNormalizations{

  "after being lowerCased" should "全ての文字を小文字に変換して比較すると等しい" in {
    "ScalaTest" should equal ("scalatest") (after being lowerCased)
  }

  "after being upperCased" should "全ての文字を大文字に変換して比較すると等しい" in {
    "Html" should equal ("HTML") (after being upperCased)
  }

  "after being trimmed" should "トリム(前後の空白文字を削除)して比較すると等しい" in {
    " Scala\t\r\n" should equal ("Scala") (after being trimmed)
  }

  "after being lowerCased and trimmed" should "lowerCasedとtrimmedを併せて比較する" in {
    " ScalaTest\t\r\n" should equal ("scalatest") (after being lowerCased and trimmed)
  }
}

文字列の一部もしくは全部が正規表現にマッチする
何らかの文字列を含むとか、指定した正規表現にマッチするとかいった検証を行いたい場合には以下の Matcher を使います。 startWith, include, endWith の使い方は同じです。 それぞれの最初のものは正規表現に無関係ですが、まぁ細かいことは気にせずに。 fullMatch は正規表現に特化していますが、やはり使い方は同じ。

  • 開始文字列の検証 startWith
    • SUT should startWith "文字列"
    • SUT should startWith regex "正規表現"
    • SUT should startWith regex ("正規表現" withGroup "文字列")
    • SUT should startWith regex ("正規表現" withGroups ("文字列1", "文字列2", ...))
  • 包含文字列の検証 include
    • SUT should include "文字列"
    • SUT should include regex "正規表現"
    • SUT should include regex ("正規表現" withGroup "文字列")
    • SUT should include regex ("正規表現" withGroups ("文字列1", "文字列2", ...))
  • 終了文字列の検証 endWith
    • SUT should endWith "文字列"
    • SUT should endWith regex "正規表現"
    • SUT should endWith regex ("正規表現" withGroup "文字列")
    • SUT should endWith regex ("正規表現" withGroups ("文字列1", "文字列2", ...))
  • 正規表現にマッチするか検証 fullMatch
    • SUT should fullMatch regex "正規表現"
    • SUT should fullMatch regex ("正規表現" withGroup "文字列")
    • SUT should fullMatch regex ("正規表現" withGroups ("文字列1", "文字列2", ...))

withGroup / withGroups は正義表現の部分に丸括弧 () によってグループを指定し、その内容を後の文字列部分に指定します。 以下のサンプルコードを見た方がわかりやすいかと。 いくつか丸括弧 () が必要になるところに注意。 ではサンプルコード。

class StringRegexMatcherSpec extends FlatSpec with Matchers{

  "startWith" should "指定した文字列で始まる" in {
    val string = "Hello, ScalaTest world!"

    string should startWith ("Hello")
    string should startWith regex "Hel*o"  // この正規表現は "Helllo" などにもマッチ
    string should startWith regex ("(.*)," withGroup "Hello")
    string should startWith regex ("(.*), (.*) " withGroups ("Hello", "ScalaTest"))
  }

  "include" should "指定した文字列を含む" in {
    val string = "Hello, ScalaTest world!"

    string should include ("ScalaTest")
    string should include regex "[Tt]est"
    string should include regex ("\\s(.*)\\s" withGroup "ScalaTest")
  }

  "endWith" should "指定した文字列で終わる" in {
    val string = "Hello, ScalaTest world!"

    string should endWith ("world!")
    string should endWith regex "wo.ld."
    string should endWith regex ("(\\w*)!" withGroup "world")
  }

  "fullyMatch" should "指定した正規表現にマッチする" in {
    val string = "Hello, ScalaTest world!"

    string should fullyMatch regex "Hello, .* world!"
    string should fullyMatch regex ("(.*), (.*) (.*)!" withGroups("Hello", "ScalaTest", "world"))
  }
}

数値 Number

数値は通常の == 演算子(メソッド)で値の等しさの検証ができますが、それに加えて大小関係を検証する Matcher も使えます。 Scala では < なども識別子として使えるので、JUnit の lesserThan などよりも見て検証内容がわかりやすいかと思います(日本人には)。 また、一般的に浮動小数点数の等値検証はあまり信頼できませんが、その代わりに値の範囲を指定して誤差を含めた等値検証を行える Matcher もあります(浮動小数点数だけでなく整数にも使えますが)。

  • 順序関係(大小関係) <, <=, >, >=
    • SUT should be < 数値
    • SUT should be <= 数値
    • SUT should be > 数値
    • SUT should be >= 数値
  • 範囲指定 +-
    • SUT should be (中心の数値 +- 偏差)
    • SUT should equal (中心の数値 +- 偏差)
    • SUT should === (中心の数値 +- 偏差)
    • SUT shouldBe 中心の数値 +- 偏差
    • SUT shouldEqual 中心の数値 +- 偏差

<, > などでは丸括弧 () はいりませんが、not を使う場合には逆に必要になります(サンプルコード参照)。 範囲指定の +- は誤差を含んだ値を書く場合の { \pm } と同じ意味です。 つまり、

x should be (a +- e)

と指定した場合、x が

  { \displaystyle
  a-e \leqq x \leqq a+e
}

の範囲に入っていれば検証が通ります。 境界値と同じ値のときは検証が通ります。

ではサンプルコード。

class NumberMatcherSpec extends FlatSpec with Matchers{

  "<, <=, >, >=" should "大小関係を比較する" in {
    // lessThan
    -1 should be < 0
    0 should not be < (0)
    1 should not be < (0)

    // lessThanOrEqualTo
    -1 should be <= 0
    0 should be <= 0
    1 should not be <= (0)

    // greaterThan
    -1 should not be > (0)
    0 should not be > (0)
    1 should be < 0

    // greaterThanOrEqualTo
    -1 should not be >= (0)
    0 should be >= 0
    1 should be < 0
  }

  // JUnit : closeTo
  "( +- )" should "指定した範囲内にある" in {
    -10 should not be (0 +- 5)
    -5 should be (0 +- 5)  // 境界値に等しい場合は検証が通る
    0 should be (0 +- 5)
    5 should be (0 +- 5)  // 境界値に等しい場合は検証が通る
    10 should not be (0 +- 5)

    10 should equal (0 +- 10)
    10 should === (0 +- 10)
    10 shouldBe 0 +- 10
    10 shouldEqual 0 +- 10
  }
}

範囲指定は丸括弧を入れておいた方が分かりやすい気がしますね。

ファイル File

ファイルに関する Matcher は ScalaTest の UserGuide には明示的に書かれていませんが、簡単に触れておきます。

  • ファイルの存在 exist
    • SUT should exist ファイル
  • ファイルの読み書き可能性 readable/writable
    • SUT should be (readable)
    • SUT should be (writable)
    • SUT shouldBe readable
    • SUT shouldBe writable

readable / writable を should be で用いる場合には丸括弧 () が必要になります。

import java.io.File

class FileMatcherSpec extends FlatSpec with Matchers{

  "exist" should "存在する" in {
    val file = File.createTempFile("exist-", ".txt")
    file should exist

    file.delete()  // ファイルを削除
    file shouldNot exist
    file should not (exist)  // not を使う場合には丸括弧 () が必要
  }

  "be readable/writable" should "読み書きができる" in {
    val file = File.createTempFile("rw-", ".txt")

    file should be (readable)
    file should be (writable)

    file shouldBe readable
    file shouldBe writable

    file.delete()  // ファイルを削除
    file should not be readable
    file should not be writable
  }
}

nio の Path にはこれらの Matcher は使えませんが、暗黙の型パラメータを使うと比較的簡単に検証を行うことができます。 そのうち機会があればやろうかと。

例外 Exception

今回の最後は、should を使って例外が投げられることを検証する方法*5。 投げられた例外を取得することもできます。 これによって例外メッセージを検証することもできます。

  • 例外が投げられることを検証 thrownBy
    • a [例外クラス] should be thrownBy { コードブロック }
    • val ex = the [例外クラス] thrownBy { コードブロック }
  • 指定されたメッセージを持つことを検証 have message
    • SUT should have ('message ("文字列"))
  • 例外が投げられないことを検証 noException
    • noException should be thrownBy { コードブロック }

コードブロックは波括弧 {} で囲わなくてもいい場合があります。 大抵の場合は囲っておいた方が分かりやすいと思いますが。 a/an や the の後の例外クラスを指定する部分は角括弧 [] で囲います。 例外が投げられないことを検証する場合は noException を使います。 ちなみに、2つ目の例外を取得する場合は should はいりません(返り値が取得できないので)。

投げられた例外のメッセージを検証したい場合は have ('message ("文字列")) を使います。 2つの丸括弧 () と message の前のクォート ' は省略できないので注意。 have は他にも使い方があります。 詳しくは後日。

class ExceptionMatcherSpec extends FlatSpec with Matchers{

  "a/an [] should be thrownBy" should "例外が投げられる" in {
    an [ArithmeticException] should be thrownBy 1 / 0
  }

  "have ('message (\"\"))" should "指定されたメッセージを持つことを検証する" in {
    val ex = new RuntimeException("実行時例外")
    ex should have ('message ("実行時例外"))
  }

  "the [] thrownBy" should "投げられた例外を取得できる" in {
    val ex = the [ArithmeticException] thrownBy 1 / 0
    ex should have ('message ("/ by zero"))
  }

  "noException should be thrownBy" should "例外がなげられない" in {
    noException should be thrownBy 0 / 1
  }
}

今回はこの辺で。 まだまだ続きます。 次回はコレクション関連の Matcher の予定。

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

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

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

  • 作者: Martin Odersky,Lex Spoon,Bill Venners,羽生田栄一,水島宏太,長尾高弘
  • 出版社/メーカー: インプレスジャパン
  • 発売日: 2011/09/27
  • メディア: 単行本(ソフトカバー)
  • 購入: 12人 クリック: 235回
  • この商品を含むブログ (46件) を見る

*1:そう言えば should メソッドを持つオブジェクトの場合はまずそうですね。 その場合はメソッド名を変えるか、明示的に convertToAnyShouldWrapper メソッド(Matchers トレイトに定義されている)によってオブジェクトをラップするかすればよいかと。

*2:SUT shouldBe theSameInstanceAs (オブジェクト) とも書けますが、丸括弧 () が必要になります。

*3:Float 値と Double 値を比較しようとするとコンパイルエラーになる、など。 ScalaTest 特化機能1 TripleEquals 「===」参照

*4:もしくは「import org.scalactic.StringNormalizations._」のようにインポートしてもよい。

*5:should を使わない方法では intercept を使うんでしたね。