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

倭マン's BLOG

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

ScalaTest も『JUnit 実践入門』もまとめて相手してやんよ (4) : アノテーションとテストのスキップ

Scala テスト JUnit

今回は JUnit と ScalaTest で提供されているアノテーションを見ていきます(目次)。 加えて ScalaTest でテストをスキップする方法も見ていきます。

この記事の内容

参考

JUnit のアノテーション

『JUnit 実践入門』 3.5 節に書かれている、JUnit が提供しているアノテーションは以下の6つ:

アノテーション 説明
@Test テストケースとなるテストメソッドに付与する
@Ignore 付与したテストクラス、もしくはテストメソッドの実行をスキップする
@Before テストケースの事前準備
@After テストケースの後処理
@BeforeClass テストクラスの事前準備
@AfterClass テストクラスの後処理

ScalaTest では、これらのうち @Ignore に対応するものがアノテーションとして提供されています(次節)。 ただし、ScalaTest の多くのテストスタイルでようには、JUnit のテストケースをメソッドとして記述しないので(Spec を除く)、テストケースに @Ignore を付与してテストをスキップすることはできません。 それに代わる方法を次々節「ScalaTest の Ignore」で見ます。

@Before, @After, @BeforeClass, @AfterClass に対応する機能は、フィクスチャを共有を目的とするトレイト BeforeAndAfter, BeforeAndAfterAll などをテストクラスにミックスインして使用します。 これらは『JUnit 実践機能』第7章テストフィクスチャあたりにたどり着けばやる予定。

@Test はテストケースとなるメソッドに付与するアノテーションですが、ScalaTest は各テストスタイルでテストケースの定義方法が定められているので、対応するアノテーションはありません。 また、@Test には2つの属性 expected (投げられるべき例外を指定する)と timeout (指定時間内にテストが終了しなければテストを失敗にする)を持たせることができますが、これに対応する機能を ScalaTest でどのように記述するかを簡単に見ておきます。

expected 属性
expected 属性は例外クラスを指定して、テストメソッドでそれが投げられたときにテストが成功するように指定します:

@Test(expected = IllegalArgumentException.class)
public void divideで50のときIllegalArgumentExceptionを送出する() throws Exception{
    Calculator sut = new Calculator();
    sut.divide(5, 0);
}

ScalaTest ではテストコード本体で intercept を使うしかなさそうです(Matcher を使わない場合):

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

もしくは少し短くして(テストコード本体の前に「in」を使うテストスタイルならどれでも可)

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

と書くこともできます。

timeout 属性
timeout 属性はテストケースの実行時間のタイムリミットを指定します。

public class TimeoutTest {

    @Test(timeout = 200L)
    public void タイムアウト内に終了すれば成功() throws Exception{
        Thread.sleep(100);
    }

    @Test(timeout = 200L)
    public void タイムアウト内に終了しなければ失敗() throws Exception{
        Thread.sleep(300);
    }
}

このコードではどちらのテストケースのタイムリミットも200ミリ秒に設定しており、1つ目のテストケースは100ミリ秒スリープするだけなのでテストを通りますが、2つ目のテストケースは300ミリ秒スリープするので失敗します。 ScalaTest で各テストケースにタイムアウトを設定する方法は*1、org.scalatest.concurrent.Timeouts トレイトの failAfter メソッドを使います:

package scalatest.tutorial.concurrent

import org.scalatest.FlatSpec
import org.scalatest.concurrent.Timeouts
import org.scalatest.time.SpanSugar._  // failAfter メソッドの引数を「200 millis」
                                       // と書けるようにするためのシュガー

class FailAfterExampleSpec extends FlatSpec with Timeouts{

  "Calculator" should "タイムアウト内に終了すれば成功" in {
    failAfter(200 millis){
      Thread.sleep(100)
    }
  }

  it should "タイムアウト内に終了しなければ失敗" in {
    failAfter(200 millis){
      Thread.sleep(300)
    }
  }
}

もしくは少し短くして

package scalatest.tutorial.concurrent

import org.scalatest.FlatSpec
import org.scalatest.concurrent.Timeouts
import org.scalatest.time.SpanSugar._

class FailAfterExampleSpec extends FlatSpec with Timeouts{

  "Calculator" should "タイムアウト内に終了すれば成功" in failAfter(200 millis){
    Thread.sleep(100)
  }

  it should "タイムアウト内に終了しなければ失敗" in failAfter(200 millis){
    Thread.sleep(300)
  }
}

とも書けます。

ScalaTest のアノテーション

次は ScalaTest で提供されているアノテーション。 ScalaTest の Scaladoc 内を探してみると、以下のようなアノテーションが見つかりました:

  • org.scalatest パッケージ
    • @Igore
    • @DoNotDiscover
    • @WrapWith
    • @TagAnnotation
    • @Finders
  • org.scalatest.tags パッケージ
    • @Retryable
    • @Slow
    • @CPU
    • @Disc
    • @Network
    • @HtmlBrowser
    • @ChromeBrowser
    • @FirefoxBrowser
    • @InternetExplorerBrowser
    • @SafariBrowser

org.scalatest.tags パッケージのアノテーションはテストのカテゴリ化に使うか、Selenium で使用するためのタグです。 『JUnit 実践入門』 10章 カテゴリ化テスト、もしくは第4部 開発プロセスの改善 のどこかでやるかと思います(たどり着けば)。

ここでは org.scalatest パッケージ内のアノテーション(のうちのいくつか)を見ていくことにします。

@Ignore
@Ignore は、テストクラスに付与して、そのテストクラスに定義されているテストケースすべてのテストをスキップします。 前回までに書いた CalculatorSpec に @Ignore を付けて

package scalatest.tutorial

import org.scalatest.{Ignore, FlatSpec}

@Ignore
class CalculatorSpec extends FlatSpec{

  "Calculator" should "multiplyで3と4の乗算結果が取得できる" in { ... }

  ...
}

実行すると

> test-only scalatest.tutorial.CalculatorSpec
[info] CalculatorSpec:
[info] Calculator
[info] - should multiplyで3と4の乗算結果が取得できる !!! IGNORED !!!
[info] - should multiplyで5と7の乗算結果が取得できる !!! IGNORED !!!
[info] - should divideで3と2の除算結果が取得できる !!! IGNORED !!!
[info] - should divideで5と0のときIllegalArgumentExceptionを送出する !!! IGNORED !!!
[info] Run completed in 8 seconds, 55 milliseconds.
[info] Total number of tests run: 0
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 0, failed 0, canceled 0, ignored 4, pending 0
[info] No tests were executed.
[success] Total time: 34 s, completed 2015/06/18 10:48:26

のように、各テストケースで実行をスキップ(無視)した旨のメッセージ「!!! IGNORED !!!」が表示されます(また、下から3行目は4つのテストがスキップされたことも分かります ignored 4)。 これでテストをスキップしたまま放置して忘れてしまうことがなくなると思います。

テストケースを指定してスキップしたい場合は次節「ScalaTest の Ignore」の方法を参照のこと。

@DoNotDiscover
@Ignore はテストの実行はしませんが、テストがあること自体は sbt や IDE などのテスト実行側には認識されています。 場合によってはテストとして認識してほしくない場合もあるかと思いますが、この場合には @DoNotDiscover を使います。 上記の CalculatorSpec で @Ignore の代わりに @DoNotDiscover を付けて実行すると

> test-only scalatest.tutorial.CalculatorSpec
[info] Run completed in 4 seconds, 600 milliseconds.
[info] Total number of tests run: 0
[info] Suites: completed 0, aborted 0
[info] Tests: succeeded 0, failed 0, canceled 0, ignored 0, pending 0
[info] No tests were executed.
[info] No tests to run for test:testOnly
[success] Total time: 20 s, completed 2015/06/18 11:01:44

のように、テストが行われないのみならず、テストがあること自体が認識されません。

@WrapWith
JUnit の org.junit.runner.RunWith と同じ機能のアノテーションです。 これは『JUnit 実践入門』 第5章 テストランナー でやります。

@TagAnnotation
テストのカテゴリ化のためのタグを作るためのアノテーションです。 org.scalatest.tags パッケージ内のアノテーションはこれをつけて作ってるんじゃないでしょうか(ソースコード見てないので知りませんが)。 『JUnit 実践入門』 第10章 カテゴリ化テスト にて。

@Finders
IDE で ScalaTest プラグインを作るときなどに使うそうです。 ここではスルー。

ScalaTest の Ignore

ScalaTest の多くのテストスタイルでは、テストケースをメソッドとして定義しないので(Spec テストスタイルではメソッドで定義する)、テストケースを指定してスキップしたい場合に @Ignore を付けるということができません。 代わりに以下のような方法で個々のテストケースをスキップすることができます:

  • テストケースの本体の前に「in」を使うテストスタイル (FlatSpec, WordSpec, FreeSpec) では、「in」 を「ignore」に変える。
  • テストケース名の主語として「it」を使うテストスタイル (FlatSpec, FunSpec) では、「it」を「ignore」に変える。
  • FunSuite テストスタイルでは、test メソッドを呼び出す代わりに ignore メソッドを呼び出してテストケースを定義する。
  • Spec テストスタイルでは、テストケースを定義するメソッド宣言に @Ignore を付与する。

まぁ、FlatSpec 以外はそんなにニーズがないかと思うので、上2つを FlatSpec でやるだけでいいでしょう。

「in」を「ignore」に変える方法
これはまぁ、そのままとしか言いようがない方法。

package scalatest.tutorial

import org.scalatest.FlatSpec

class CalculatorSpec extends FlatSpec{

  "Calculator" should "multiplyで3と4の乗算結果が取得できる" ignore {  // in を ignore に変更
    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)
    }
  }

  ...
}

1つ目のテストケースで「in」を「ignore」に変えてます。 テストを実行すると

> test-only scalatest.tutorial.CalculatorSpec
[info] CalculatorSpec:
[info] Calculator
[info] - should multiplyで3と4の乗算結果が取得できる !!! IGNORED !!!
[info] - should multiplyで5と7の乗算結果が取得できる
[info] - should divideで3と2の除算結果が取得できる
[info] - should divideで5と0のときIllegalArgumentExceptionを送出する
[info] Run completed in 5 seconds, 648 milliseconds.
[info] Total number of tests run: 3
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 3, failed 0, canceled 0, ignored 1, pending 0
[info] All tests passed.
[success] Total time: 23 s, completed 2015/06/18 12:39:21

となり、1つ目のテストがスキップされているのが分かります。

「it」を「ignore」に変える方法
次はテストケースの定義に「it」を使っている場合に使える方法。 FlatSpec テストスタイルだと上記の方法で良さそうだけど、今の方法ではテストケース定義の頭に「ignore」を付けるので、どのテストケースがスキップされるのか探しやすいのが利点かと。 2つ目以降のテストケースに対しては特に問題なし:

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)
  }

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

  ...
}

これを実行すると

> test-only scalatest.tutorial.CalculatorSpec
[info] CalculatorSpec:
[info] Calculator
[info] - should multiplyで3と4の乗算結果が取得できる
[info] - should multiplyで5と7の乗算結果が取得できる !!! IGNORED !!!
[info] - should divideで3と2の除算結果が取得できる
[info] - should divideで5と0のときIllegalArgumentExceptionを送出する
[info] Run completed in 10 seconds, 431 milliseconds.
[info] Total number of tests run: 3
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 3, failed 0, canceled 0, ignored 1, pending 0
[info] All tests passed.
[success] Total time: 31 s, completed 2015/06/18 12:53:24

まぁ、これは問題ないですね。 この方法でマズいのは、1つ目のテストケースでは「it」を使っていないので、「ignore」に変えられないところ。 この場合は「behavier of」を使って少しコードを書き換える必要があります:

package scalatest.tutorial

import org.scalatest.FlatSpec

class CalculatorSpec extends FlatSpec{

  behavior of "Calculator"

  ignore should "multiplyで3と4の乗算結果が取得できる" in {  // it を ignore に変更
    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)
    }
  }

  ...
}

これを実行すれば、1つ目のテストケースがスキップされます。

今回は JUnit と ScalaTest で使われているアノテーションを見てきました。 ScalaTest のアノテーションの多くは、何かしらのトレイトをミックスインするなどの代替手段があるようですが、一部にアノテーションを付与することでしか使えない機能もあるようです。 まぁ、独自アノテーションを作るなら Java コードを書いたりする必要がありますが、用意されたものを使う分には使うのは簡単ですね。 あとは、どんなアノテーションがあるのかをキチンと把握しておくだけです。

また、最後にはアノテーションとは離れて、テストをスキップする方法を見ました。 ScalaTest では、テストのスキップと似た機能で「テストの pending (保留)」という機能があります。 これも併せてやろうと思ったのですが、記事が長くなりすぎたので『JUnit 実践入門』 第16章 テスト駆動開発までとっておきます、、、全くたどり着ける気がしないですが(笑)

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:org.scalatest.concurrent.TimeLimitedTests トレイトを使えば、1つのテストクラス内のテストケースに共通するタイムアウトを設定できます。 これは JUnit の Timeout ルールの設定と大体同じなので、第9章で見ていきます(たどり着けば)。