倭マン's BLOG

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

ScalaTest も『JUnit 実践入門』もまとめて相手してやんよ (3) : 可読性の高いテストコードの書き方

今回はテストコードを書く上で念頭に置いておきたい概念、ステップなど(目次)。 詳しくは『JUnit 実践入門』本文参照。 ここではコードを中心に機械的な話だけ。

この記事の内容

参考

テスト対象 SUT

テストコードではどのオブジェクトがテスト対象化をわかりやすくするため、テスト対象のオブジェクトに「sut」(System Under Test) と名前を付けておくといいそうで。 同様に、実測値には「expected」、期待値には「expected」と名前を付ける。 また、コンストラクタのテストでは sut と expected が同じオブジェクトなので、特に「instance」と名前を付ける(『JUnit 実践入門』 3.6 JUnit のテストパターン)。 前回までに書いた CalculatorSpec を書き換えると以下のようになります:

package scalatest.tutorial

import org.scalatest.FlatSpec

class CalculatorSpec extends FlatSpec{

  "Calculator" should "multiplyで3と4の乗算結果が取得できる" in {
    val sut = new Calculator
    val expected = 12
    val actual = sut.multiply(3, 4)
    assert(actual == expected)
  }

  it should "multiplyで5と7の乗算結果が取得できる" in {
    val sut = new Calculator
    assertResult(35) {
      sut.multiply(5, 7)
    }
  }

  it should "divideで3と2の除算結果が取得できる" in {
    val sut = new Calculator
    val expected = 1.5f
    val actual = sut.divide(3, 2)
    assert(actual == expected)
  }

  it should "divideで5と0のときIllegalArgumentExceptionを送出する" in {
    val sut = new Calculator
    intercept[IllegalArgumentException] {
      sut.divide(5, 0)
    }
  }
}

4フェーズテストと GivenWhenThen トレイト

テストコードは次の4つのステップを念頭においてコードを書くべし。

  1. 事前準備 (set up)
  2. 実行 (exercise)
  3. 検証 (verify)
  4. 後処理 (tear down)

テストコードを読むときにも気にすべきなんでしょうが、もういっそのことコードにコメント書き入れることを習慣にしておくべし、とのこと。 では、『JUnit 実践入門』に断片が書かれている ArrayList のコードを ListBuffer で書き換えたサンプル:

package scalatest.tutorial

import scala.collection.mutable
import org.scalatest.{GivenWhenThen, FlatSpec}

class ListBufferSpec extends FlatSpec{

  "ListBuffer" should "+=で要素を追加するとサイズが1となりheadで取得できる" in {
    // SetUp
    val sut = mutable.ListBuffer.empty[String]
    // Exercise
    sut += "Hello"
    // Verify
    assert(sut.size == 1)
    assert(sut.head == "Hello")
  }

  it should "要素が2つ追加された状態で要素をremoveするとsizeが1となる" in {
    // SetUp
    val sut = mutable.ListBuffer("Hello", "World")
    // Exercise
    sut.remove(0)
    // Verify
    assert(sut.size == 1)
    assert(sut.head == "World")
  }
}

ちなみに、2つ目のテストケースでは、助長だけど以下のようにも書けます:

  it should "要素が2つ追加された状態で要素をremoveするとsizeが1となる" in {
    // SetUp
    val sut = mutable.ListBuffer.empty[String]
    sut += "Hello"
    sut += "World"
    // Exercise
    sut.remove(0)
    // Verify
    assert(sut.size == 1)
    assert(sut.head == "World")
  }

Java の ArrayList などは(Arrays#asList メソッドなどを使わない限り)空の ArrayList オブジェクトを作って要素を順次追加していく必要がありますが、「要素が2つ追加された状態」のオブジェクトをテストする場合には、これらの要素を追加する操作は事前準備 (set up) のフェーズになりますね。 実行 (exercise) フェーズでは、テストする操作(今の場合 remove メソッド)だけを行います。

さて、このくらいのテストだとちょっとコメントを入れておくだけで充分わかりやすくなりますが、複雑なテストだともう少しコメントを補足したい場合や、テスト結果の出力に情報を追加したい場合などがあります。 そんなときのために、ScalaTest では GivenWhenThen トレイトというのが用意されています。 概ね

  • Given ・・・ 事前準備 (set up)
  • When ・・・ 実行 (exercise)
  • Then ・・・ 検証 (verify)

に対応していると思います(というかそのまま)。 事後処理 (tear down) に対応するものはありません。 また、この3つの他に And というのもありますが、これは英単語の And と同じ役割(出力の問題)。 これを使って上記のテストを書き換えてみると

package scalatest.tutorial

import scala.collection.mutable
import org.scalatest.{GivenWhenThen, FlatSpec}

class ListBufferSpec extends FlatSpec with GivenWhenThen{

  "ListBuffer" should "+=で要素を追加するとサイズが1となりgetで取得できる" in {
    Given("空のListBuffer")
    val sut = mutable.ListBuffer.empty[String]

    When("要素を1つ追加")
    sut += "Hello"

    Then("サイズが1")
    assert(sut.size == 1)

    And("最初の要素が加えた要素")
    assert(sut.head == "Hello")
  }

  it should "要素が2つ追加された状態で要素をremoveするとsizeが1となる" in {
    Given("要素が2つのListBuffer")
    val sut = mutable.ListBuffer("Hello", "World")

    When("要素を削除")
    sut.remove(0)

    Then("サイズが1")
    assert(sut.size == 1)

    And("最初の要素が2番目に加えられた要素")
    assert(sut.head == "World")
  }
}

これを実行すると

> test-only scalatest.tutorial.ListBufferSpec
[info] ListBufferSpec:
[info] ListBuffer
[info] - should +=で要素を追加するとサイズが1となりgetで取得できる
[info]  + Given 空のListBuffer
[info]  + When 要素を1つ追加
[info]  + Then サイズが1
[info]  + And 最初の要素が加えた要素
[info] - should 要素が2つ追加された状態で要素をremoveするとsizeが1となる
[info]  + Given 要素が2つのListBuffer
[info]  + When 要素を削除
[info]  + Then サイズが1
[info]  + And 最初の要素が2番目に加えられた要素
[info] Run completed in 2 seconds, 646 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 21 s, completed 2015/06/10 10:34:44

のように、各ステップの情報が出力されます。 もし事後処理など、Given, When, Then, And で書けないものがある場合は、「info("《メッセージ》")」で同様のメッセージを出力させることができます。

番外編

上記のコードでは、出力に Given, When, Then, And などの英単語がそのまま出力されてしまっているので、ちょっとこのあたりも日本語にしてみましょう。 あくまで遊びだけど。

まず、GivenWhenThen トレイトを使うと強制的に上記の英単語が出力されるので、新たにトレイトを作ります:

package scalatest.tutorial.info

import org.scalatest.Informing

trait JapaneseInformer extends Informing{

  def 事前準備() = info("事前準備")
  def 事前準備(message:String) = info("事前準備:" + message)

  def 実行() = info("実行")
  def 実行(message:String) = info("実行:" + message)

  def 検証() = info("検証")
  def 検証(message:String) = info("検証:" + message)

  def 後処理() = info("後処理")
  def 後処理(message:String) = info("後処理:" + message)

  def また() = info("また")
  def また(message:String) = info("また:" + message)
}

引数なしのメソッドも便利のため定義しておきました。 これらのメソッドの処理は、前節最後に言及した「info」*1によって文字列を出力するだけのものです。 これを使って ListBufferSpec を書き換えると以下のようになります:

package scalatest.tutorial.info

import scala.collection.mutable
import org.scalatest.FlatSpec

class ListBufferSpec extends FlatSpec with JapaneseInformer{

  "ListBuffer" should "+=で要素を追加するとサイズが1となりgetで取得できる" in {
    事前準備()
    val sut = mutable.ListBuffer.empty[String]

    実行()
    sut += "Hello"

    検証()
    assert(sut.size == 1)
    assert(sut.head == "Hello")
  }

  it should "要素が2つ追加された状態で要素をremoveするとsizeが1となる" in {
    事前準備("要素が2つのListBuffer")
    val sut = mutable.ListBuffer("Hello", "World")

    実行("要素を削除")
    sut.remove(0)

    検証("サイズが1")
    assert(sut.size == 1)

    また("最初の要素が2番目に加えられた要素")
    assert(sut.head == "World")
  }
}

1つ目のテストケースでは追加メッセージなし(引数なし)で、2つ目のテストケースでは追加メッセージありでコードを書いてます。 これを実行すると、出力は以下のようになります:

> test-only scalatest.tutorial.info.ListBufferSpec
[info] ListBufferSpec:
[info] ListBuffer
[info] - should +=で要素を追加するとサイズが1となりgetで取得できる
[info]  + 事前準備
[info]  + 実行
[info]  + 検証
[info] - should 要素が2つ追加された状態で要素をremoveするとsizeが1となる
[info]  + 事前準備:要素が2つのListBuffer
[info]  + 実行:要素を削除
[info]  + 検証:サイズが1
[info]  + また:最初の要素が2番目に加えられた要素
[info] Run completed in 11 seconds, 782 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 39 s, completed 2015/06/10 11:12:56

思ったよりはいい感じ。github.com

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:今の場合、Informing をミックスインしているので、フィールドとして参照可能。 メッセージを出力するためには、この info フィールドの apply メソッドを呼び出す。