パーソナルツール
現在の場所: ホーム Users YAMADA Masaki 2009 0912 SpockInJapanese SpockBasics
ログイン


パスワードを忘れた?
 

SpockBasics

— カテゴリー:

Groovy/Grailsの振る舞い駆動開発 (Behavior-driven Development, BDD) フレームワーク Spockの使い方 (邦訳)

Spockの基礎

Spock仕様を解剖しよう

この章では, GroovyとUnitテストについての基本的な知識は持っているものとします. もし, あなたがJavaプログラマで, Groovyについては何も知らないとしても, 心配は要りません. Groovyはあなたにとってもとてもなじみ深いものになると思いますよ! 実際, Groovyの主要な設計目標の一つはJavaに沿ったスクリプト言語となることなのです. 必要に応じて Groovyのドキュメント を参照してください.

この章の目的は

  • 現実的なSpock仕様が書けるまでSpockについて学ぶ
  • あなたの好奇心をかき立てる
ことです.

Groovyについてもっとよく知るためには, http://groovy.codehaus.org を見てください.

ユニット・テスティングについてもっとよく知るためには, http://en.wikipedia.org/wiki/Unit_testing を見てください.

用語 Terminology

まずはいくつかの定義から始めましょう: Spockを用いて, 関心の対象であるシステムが提供することが期待されるフィーチャ (プロパティ, アスペクト) を記述する仕様を書くことができます. 関心の対象であるシステムは, クラスだったり, アプリケーション全体だったりしますが, これを仕様対象システム (system under specification, SUS) と呼びます. フィーチャの記述はSUSとそのコラボレータの特定のスナップショットから始めます. このスナップショットのことを, そのフィーチャのフィクスチャと呼びます.

以下の節では, Spock仕様を構成する要素を一通り見てみます. 普通の仕様で用いるのは, その一部だけです.

インポート Imports

import spock.lang.*

パッケージ spock.lang には仕様を書くためにもっとも必要な型が含まれています.

仕様 Specification

class MyFirstSpecification extends Specification {
  // フィールド
  // フィクスチャ メソッド
  // フィーチャ メソッド
  // ヘルパ メソッド
}

仕様は, spock.lang.Specification をextendしたGroovyクラスで表します. 仕様の名前は, 通常, 仕様で記述しようとしているシステムやシステムの操作に関係するものにします. 例えば CustomerSpec, H264VideoPlayback, ASpaceshipAttackedFromTwoSides などは, 仕様によっては適切な名前と言えるでしょう.

クラス Specification (と, そのスーパークラス Predef) には, 仕様を書くのに役立つ多くのメソッドが含まれています. さらに, JUnitを Sputnik (SpockのJUnit runner) を使って仕様を走らせるようにしています. Sputnikのおかげで, Spock仕様はほとんどのモダンなIDEやビルド・ツールで動かすことができます (現状と今後の見通しについては Getting started を参照のこと)

注意: 仕様を宣言する別のやり方もあります:

@Speck
@RunWith(Sputnik)
class MyFirstSpecification { ... }

が, Specification をextendする方を使うようにしてください.

フィールド Fields

def obj = new ClassUnderSpecification()
def coll = new Collaborator()

仕様のフィクスチャに属するオブジェクトを保存するのには, インスタンス・フィールドを利用します. 宣言したら, すぐにその場で初期化しておくのがいいでしょう (意味論的には, これはsetup() のいちばん最初に初期化するのと同じことです). インスタンス・フィールドに格納したオブジェクトは, フィーチャ・メソッドの間では共有されません. フィーチャ・メソッドは, それぞれにオブジェクトを持ちます. これによって, 各フィーチャ・メソッドを隔離することができ, たいていはそれが望ましいはずです.

@Shared res = new VeryExpensiveResource()

とは言え, フィーチャ・メソッド間でオブジェクトを共有したい場合もあります. 例えば, 生成に非常にコストが掛かるとか, フィーチャ・メソッド間でインタラクションをさせたいような場合です. このためには, フィールドを @Shared 宣言します. この場合も, 宣言したら, すぐにその場で初期化しておくのがいいでしょう (意味論的には, これはsetupSpeck() のいちばん最初に初期化するのと同じことです).

static final PI = 3.141592654

静的フィールドは, 定数の宣言だけに使い, それ以外の場合は共有フィールドを使います. 共有に関するセマンティクスはより明確に定義されているからです.

フィクスチャ・メソッド Fixture Methods

def setup() {}          // run before every feature method
def cleanup() {}        // run after every feature method
def setupSpeck() {}     // run before the first feature method
def cleanupSpeck() {}   // run after the last feature method

フィクスチャ・メソッドは, フィーチャ・メソッドの走る環境を設定し, 後始末する責務を持ちます. 通常はフィーチャ・メソッドごとに新しいフィクスチャを利用するのがいいので, そのために setup() と cleanup() を使います. 場合によっては, フィーチャ・メソッド間でフィクスチャを共有するのがいい場合もあります. このときには, setupSpeck() と cleanupSpeck() メソッドを使って, 共有フィールドの設定と後始末をします. フィクスチャ・メソッドはどれもなくても構いません.

注意: setupSpeck() と cleanupSpeck() メソッドからはインスタンス・フィールドを参照できません.

フィーチャ・メソッド Feature Methods

def "pushing an element on the stack"() {
  // blocks go here
}

フィーチャ・メソッドこそは, 仕様の中心部です. 仕様対象システムに期待されるフィーチャ (あるいはプロパティ, アスペクト) を記述します. 習慣として, フィーチャ・メソッド名には文字列リテラルを用います. こうすれば, フィーチャ・メソッド名にはどんな文字でも使えますから, フィーチャを表す最適な名前を付けるようにしてください.

概念的には, フィーチャ・メソッドには四つのフェーズがあります:

  • フィーチャのフィクスチャを設定する
  • 仕様対象システムの 刺激 stimulus を用意する
  • システムに期待される 反応 response を記述する
  • フィーチャのフィクスチャの後始末をする
最初と最後のフェーズはなくても構いませんが, 中の二つのフェーズは (フィーチャ・メソッド間のインタラクションの場合を除いて) 常に必要で, 複数回ある場合もあります.

ブロック Blocks

Blocks2Phases

Spockには, フィーチャ・メソッドのフェーズ概念のそれぞれを実現するための仕掛けが組み込まれています. この目的のために, フィーチャ・メソッドをいわゆるブロックから構成するようにします. 次の6種類のブロックがあります: setup, when, then, expect, cleanup, where. メソッドの頭から, 最初の明示的ブロックまでの間にある文は, すべて暗黙的に setup ブロックに属します.

フィーチャ・メソッドは少なくともひとつの明示的ブロック (つまりラベルの付いたブロック) を持たなければなりません. 実際, 明示的ブロックのあるメソッドがフィーチャ・メソッドとなるようになっています. ブロックによってメソッドを複数のセクションに分割することになりますが, それを入れ子にすることはできません.

右の図はフィーチャ・メソッドの概念上のフェーズとブロックとの間の対応関係を示しています. where ブロックは特別な役割を果たしていますが, それについてはすぐ後に述べます. その前に, まずは他のブロックについて見ておきましょう

Setup ブロック

setup:
def stack = new Stack()
def elem = "push me"

setup ブロックは, これから記述しようとしているフィーチャの設定作業を行う場所です. 最初のブロックでなければならず, 複数回書くこともできません. setup ブロックには特別なセマンティクスはありません. setup ラベルはなくてもよく, その場合には暗黙的な setup ブロックとなります. given: ラベルは, setup: の別名で, これを使うことによって, より読みやすいフィーチャ・メソッド記述となる場合もあります (ドキュメントとしての仕様の節を参照).

When/Then ブロック

when:   // stimulus
then:   // response

when ブロックと then は常にペアで, それぞれ刺激と, 期待される応答を記述します. when ブロックには任意のコードを書くことができますが, then ブロックに書けるのは 条件 conditions, 例外条件 exception conditions, インタラクション interactions と変数定義だけです. フィーチャ・メソッドには複数の when-then ブロックのペアを書くことができます.

条件 Conditions

条件は, 期待される状態を記述するもので, JUnitのアサーションと似ています. しかし, 条件は単なるブール式として書き, アサーションAPIは使いません (より正確には, 条件はブール値以外の値を生成する場合もありますが, その場合にはGroovy流の真偽値として評価されます). 実際の条件の例を見てみましょう.

when:
stack.push(elem)

then:
!stack.empty
stack.size() == 1
stack.peek() == elem

ヒント: フィーチャ・メソッドごとの条件の数はできるだけ少なくしよう. ガイドラインとしては1から5くらい. これ以上書きたくなったら, 関連のない複数のフィーチャをひとまとめに書こうとしていないか, 自問自答してみよう. もし答えがイエスならば, フィーチャ・メソッドを複数に分割する. もしその条件が値だけ違っているようなものならば, 次の方法が使えないか, 検討してみること. parameterized feature methods

条件が満たされなかった場合には, Spockはどのような種類のフィードバックを返すのでしょうか? 2番目の条件を stack.size() == 2 に変えてみましょう. 次のようになるはずです:

Condition not satisfied:

stack.size() == 2
|     |      |   
|     1      false
[push me]

ご覧のように, Spockは条件の評価中に生成された値をすべて把握して, 簡単に要約できるような形式で表示します. なかなかいいでしょ?

暗黙的条件と明示的条件 Implicit and explicit conditions

条件は then ブロックと expect ブロックの本質的な原料です. void メソッドの呼び出しとインタラクションとして分類される式を除いて, これらのブロック中のトップ・レベルの式は暗黙的に条件として扱われます. 条件を他の場所で使う場合には, Groovyの assert キーワードで指定する必要があります:

def setup() {
  stack = new Stack()
  assert stack.empty
}

明示的な条件が満たされなかった場合には, 暗黙的な条件の場合と同様の素敵な診断メッセージが生成されます.

例外条件 Exception Conditions

例外条件は, when ブロックが例外を投げるべきであることを示すときに使います. これは, thrown() に予期される例外の型を渡すことで定義します. 例えば, 空のスタックからpopしようとした場合に EmptyStackException が投げられることを記述するには, 次のように書きます:

when:
stack.pop()

then:
thrown(EmptyStackException)
stack.empty

ここで見たように, 例外条件の後には, 他の条件 (や他のブロック) を続けて書くことができます. これは期待される例外の内容を指定するときに, 特に有用です. 例外にアクセスするには, まずそれを変数に束縛します:

when:
stack.pop()

then:
def e = thrown(EmptyStackException)
e.cause == null

あるいは, 上のシンタックスの変種として, 次のように書くこともできます:

when:
stack.pop()

then:
EmptyStackException e = thrown()
e.cause == null

このシンタックスには, 二つのちょっとした利点があります. 一つは, 例外変数は強く型付けされるので, IDEがコード補完をしやすくなること. もう一つは, 条件がちょっと文章のように読めること (「その時, EmptyStackException が投げられる」). thrown() メソッドに例外の型を渡さない場合には, 左辺の変数の型から推論されることに注意.

場合によっては, 例外が投げられるべきではないということを明確にしたいときもあります. 例えば, HashMap が null キーを受け入れることを表すとすると:

def "HashMap accepts null key"() {
  setup:
  def map = new HashMap()
  map.put(null, "elem")
}

これでも動作はしますが, コードの意志を伝えきっていません. このコードを書いた人はこのメソッドの実装を終える前にどこかに行っちゃったのかな? いったい条件はどこ? 幸い, 次のようにもっとうまく書くことができます:

def "HashMap accepts null key"() {
  setup:
  def map = new HashMap()
  
  when:
  map.put(null, "elem")
  
  then:
  notThrown(NullPointerException)
}

notThrown() を使うと, 特に NullPointerException が投げられないはず, ということを明確にできます (Map.put() の契約により, null キーをサポートしていないマップに対してこれは正しい). ただし他の例外が投げられた場合には, このメソッドは失敗することになります.

インタラクション Interactions

条件があるオブジェクトの状態を示すものであるのに対して, インタラクションは複数のオブジェクトが互いにどのようにコミュニケートし合うのかを示すものです. インタラクションについては, この全体を費やして述べていますが, ここでは短い例題を挙げるのに留めておきましょう. さて, pubilsherからsubscriberへのイベントの流れを書きたいものとします. こんなコードになります:

def "events are published to all subscribers"() {
  def subscriber1 = Mock(Subscriber)
  def subscriber2 = Mock(Subscriber)
  def publisher = new Publisher()
  publisher.add(subscriber1)
  publisher.add(subscriber2)
  
  when:
  publisher.fire("event")
  
  then: 
  1 * subscriber1.receive("event")
  1 * subscriber2.receive("event")
}

Expect ブロック

expect ブロックは, then ブロックよりさらに制限されており, 条件と変数定義しか書けません. 刺激とそれに期待される応答を一つの式に書くのが自然であるような場合に有用です. 例えば, Math.max() メソッドの次の二つの書き方を較べてみてください.

when:
def x = Math.max(1, 2)

then:
x == 2
expect:
Math.max(1, 2) == 2

どちらのスニペットも意味的には等価ですが, 二番目の方が明らかにいいですよね. 判断基準としては, 副作用があるメソッドを記述する場合には when-then を使い, 純関数的なメソッドを記述する場合には expect を使うのがいいでしょう.

ヒント: any() や every() のような Groovy JDK のメソッドを活用すると, より表現力が豊かで, 簡潔な条件を書くことができるよ.

Cleanup ブロック

setup:
def file = new File("/some/path")
file.createNewFile()

// ...

cleanup:
file.delete()

cleanup ブロックの後には where ブロックのみ書くことができ, しかも複数回書くことはできません. cleanup メソッドと同様, フィーチャ・メソッドで利用したリソースをすべて解放するために用い, 例えフィーチャ・メソッド (のこれ以前の部分) で例外を起こしたとしても呼ばれます. これにより, cleanup ブロックは, 防衛的なコードにしなければなりません. つまり, 最悪の場合, フィーチャ・メソッドの最初の文が例外を投げ, 局所変数がデフォルト値のままだった場合でもうまく対処する必要があるのです.

ヒント: Groovyの"参照先の値の安全な取得"演算子 (foo?.bar()) を使うと防衛的なコードを簡単に書けるよ.

オブジェクト・レベルでの仕様では, 通常 cleanup メソッドを使いません. 消費するリソースはメモリのみで, これはがべっじ・コレクタが自動的に回収してくれるからです. ただし, より粗粒度の仕様では, ファイル・システムの後始末をしたり, データベースへの接続を閉じたり, ネットワーク・サービスを切断したりするために, cleanup ブロックを使う必要があります.

ヒント: もし, すべてのフィーチャ・メソッドが同じリソースを要求するように設計された仕様では, (cleanup ブロックではなく) cleanup() メソッドを使いなさい. setup() メソッドと setup ブロックに適用されるトレードオフも同様.

Where ブロック

where ブロックは, 常にメソッドの最後に書き, 複数回書くことはできません. データ駆動型のフィーチャ・メソッドを書くのに用います. それがどういうものかは, 次の例を見てみてください:

def "computing the maximum of two numbers"() {
  expect:
  Math.max(a, b) == c

  where:
  a << [5, 3]
  b << [1, 9]
  c << [5, 9]
}

この where ブロックは, フィーチャ・メソッドの二つの"バージョン"を効率よく作っています: つまり一つのバージョンは a が5で, b が1で, c が5. もう一つのバージョンは a が3で, b が9で, c が9.

where ブロックに関しては, Parameterizations の章で詳しく書きます.

ヘルパー・メソッド

フィーチャ・メソッドを書いているうちに巨大化したり, 重複コードが増えていったりすることがあります. このような場合, ヘルパー・メソッドを導入するといいでしょう. ヘルパー・メソッドの候補を例えば二つあげるとすれば, 例えば設定/後始末のロジックや複雑な条件などです. 前者の括り出しはそのまんまなので, ここでは後者の条件について見てみましょう.

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()
  
  then:
  pc.vendor == "Sunny"
  pc.clockRate >= 2333
  pc.ram >= 4096
  pc.os == "Linux"
}

もしあなたがPCオタクで, PCの好みの構成がすごく細かいところまで及んでいるとか, 複数のお店の見積を較べたいとかだとしましょう. このとき, 上の条件を括り出すことができます.

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()
  
  then:
  matchesPreferredConfiguration(pc)
}

def matchesPreferredConfiguration(pc) {
  pc.vendor == "Sunny"
  && pc.clockRate >= 2333
  && pc.ram >= 4096
  && pc.os == "Linux"
}

ここで導入したヘルパー・メソッド matchesPreferredConfiguration() にあるのは結果を返すブール式ひとつだけです (Groovy 1.5以降では return キーワードは要らないんだよね). この中にひとつだけ都合の合わない条件があるとしましょう:

Condition not satisfied:

matchesPreferredConfiguration(pc)
|                             |
false                         ...

でもこれじゃよく分かりませんね. こうすれば, よくなります:

void matchesPreferredConfiguration(pc) {
  assert pc.vendor == "Sunny"
  assert pc.clockRate >= 2333
  assert pc.ram >= 4096
  assert pc.os == "Linux"
}

条件をヘルパー・メソッドに括り出すとき, 次の二つの点をよく考えなければなりません. まず暗黙的な条件を assert キーワードを用いた明示的な条件に変換しなければならないこと. 次に, ヘルパー・メソッドの返す型は void でなければならないこと. そうしないと, Spockは戻り値を条件の不充足と見なしてしまいますが, これは意図するところではないですよね.

手を入れたヘルパー・メソッドは, 期待通り問題点を指摘してくれます.

Condition not satisfied:

assert pc.clockRate >= 2333
       |  |         |
       |  1666      false
       ...

最後の忠告: コードの再利用は一般的には良いことだとしても, やり過ぎちゃ駄目. フィクスチャとヘルパー・メソッドの利用はフィーチャ・メソッド間の結合度を大きくする可能性があることに注意. 再利用をやり過ぎたり, 間違ったコードを再利用したりすると, 壊れやすくて, 保守が難しい仕様になってしまう.

ドキュメントとしての仕様

ちゃんと書いた仕様は, 貴重な情報源になります. 特に単なる開発者よりも広範な読者 (アーキテクト, 分野の専門家, 顧客など) を対象とした上位レベルの仕様では, 単に仕様とフィーチャの名前だけではなく, 自然言語で情報を提供することに意味があります. そこでSpockはブロックにテキスト記述を付けられるようにしています.

setup: "open a database connection"
// code goes here

ブロックの部分ごとに and: で書くことができます:

setup: "open a database connection"
// code goes here

and: "seed the customer table"
// code goes here

and: "seed the product table"
// code goes here

and: ラベルとそれに続く記述は, フィーチャ・メソッドのどの (トップ・レベルの) 場所にでも, メソッドの意味を変えることなく挿入できます.

振る舞い駆動開発では, 顧客が目にするフィーチャ (ストーリ と呼ぶ) は, given-when-thenの形式で書きます. Spockは given: ラベルで, このような仕様の書き方がそのままできるようにしています:

given: "an empty bank account"
// ...

when: "the account is credited $10"
// ...

then: "the account's balance is $10"
// ...

前にも書いたように, given: は単に setup: の別名です.

ブロックの記述は, ソース・コードだけではなく, Spock実行時にも存在します. ブロック記述の計画的な利用は, 診断メッセージとすべてのステークホルダが等しく理解できるようなテキスト・リポートを使うことによってより充実します.

拡張

ここまで見てきたように, Spockは仕様を書くための多くの機能性を提供しています. しかし, いつかはこれ以外の機能も必要とされるようになります. そのために, Spockは割り込み (interception) に基づく拡張機構を用意しています. ディレクティブ directives と呼ばれるアノテーションによって拡張機能が呼ばれます. 現在, Spockには以下のディレクティブが最初から入っています.

  • @Timeout
フィーチャ・メソッドあるいはフィクスチャ・メソッド実行のタイムアウトを指定する.
  • @Ignore
フィーチャ・メソッドを無視する.
  • @IgnoreRest
このアノテーションのないすべてのフィーチャ・メソッドを無視する. 一つだけのメソッドをすぐに走らせるのに便利.
  • @FailsWith
そのフィーチャ・メソッドが失敗することを期待する. @FailsWith には二つの場合がある. 一つはすぐには解決できないバグがあることを文書化したい場合. もう一つは特定の場合に例外条件を置き換える (例外条件の振る舞いを指定する場合など). それ以外の場合には例外条件を使うべき.

ディレクティブと拡張の実装方法については以下の章を見てください. Extensions

JUnitとの比較対照表

Spockが使う用語は異なるものの, 考え方やフィーチャの多くはJUnitからインスパイアされたものです. おおざっぱな対照は次の通りです

Spock JUnit
Specification Test class
setup() @Before
cleanup() @After
setupSpeck() @BeforeClass
cleanupSpeck() @AfterClass
Feature Test
Parameterized feature Theory
Condition Assertion
Exception condition @Test(expected=...)
@FailsWith @Test(expected=...)
Interaction Mock expectation (EasyMock, JMock, ...)

original: http://code.google.com/p/spock/wiki/SpockBasics

translated into Japanese by masaki@metabolics.co.jp at 2009.12.06