- Spock とは
- build.gradle
- HelloSpock
- 仕様(テストケース)の構造
- データドリブンなテスト
- Stack の仕様例
- 例外の検証
- 例外とならないことの検証
- タイムアウトを適用する
- Hamcrest matchers を使う
- リソースのクリーンナップを行う
- @Shared で共有リソースを利用する
- Specification に自然言語の名前をつける
- MOPを適用する
- feature の実行を制限する
Spock とは
- Java と Groovy アプリケーションのテスティングと仕様フレームワーク
- JUnit, jMock, RSpec, Groovy, Scala, Vulcans などにインスパイアされた
- Groovy の DSL による可読性の高いテストコードが書ける
- メジャー IDE との連携も十分
- 強力なデータドリブンテストとモック機能を提供
- 2015年に1.0リリース
Githubはこちら
build.gradle
最低限の Gradle ビルドスクリプトは以下のようになります。
apply plugin: "groovy" repositories { mavenCentral() } dependencies { compile "org.codehaus.groovy:groovy-all:2.4.1" testCompile "org.spockframework:spock-core:1.0-groovy-2.4" }
Hamcrest Matcher を利用する場合は以下の依存を追加します。
testCompile "org.hamcrest:hamcrest-core:1.3"
モックを使う場合は以下も追加します。
testRuntime "cglib:cglib-nodep:3.1" testRuntime "org.objenesis:objenesis:2.1"
HelloSpock
最初に簡単なテスト仕様を見てみましょう。
src/test/groovy/HelloSpock.groovy
import spock.lang.* class HelloSpock extends spock.lang.Specification { def "length of Spock's and his friends' names"() { expect: name.size() == length where: name | length "Spock" | 5 "Kirk" | 4 "Scotty" | 7 } }
expect
でテストgreenの条件を記載し、where
にて条件に与える変数を指定しています。
gradle test
でテストを実行してみると
Condition not satisfied: name.size() == length | | | | | 6 | 7 Scotty false
失敗しました。
Scotty
の文字長は 6 文字なのでテスト条件の間違いです。
Spock は失敗したテストに対して、このような詳細なレポートを提供してくれます。
仕様(テストケース)の構造
JUnit における Test クラスは、Spock では Specification(仕様)と呼びます。
仕様を定義するために以下をインポート宣言に追加します。
import spock.lang.*
仕様は以下のように Specification を継承したクラスとして定義します。
class MyFirstSpecification extends Specification { // フィールド // fixture メソッド // feature メソッド // helper メソッド }
仕様の中には、フィールド定義、fixture メソッド、feature メソッド、helper メソッド を書きます。
定義した仕様は Sputnik
という JUnit runner を通してテストが実行されます。
フィールド
仕様や fixture で利用するオブジェクトを宣言して初期化するのに良い場所です。 例えば以下のように、単に任意の定義をフィールド値として書くだけです。
def obj = new ClassUnderSpecification() def coll = new Collaborator()
ここでの定義はsetup()メソッドで変数を初期化するのと同じ意味となります。
feature メソッド間で共有したい場合は以下のようにします。
@Shared res = new VeryExpensiveResource()
これは後述する setupSpec()
メソッドによる初期化と同じ意味となります。
fixture メソッド
feature メソッドで実行時の環境設定を行います。 JUnit での @Before/@After, @BeforeClass/@AfterClass に該当します。
メソッド | 説明 |
---|---|
def setup() {} | 全ての feature メソッドの前に実行される |
def cleanup() {} | 全ての feature メソッドの後に実行される |
def setupSpec() {} | 最初の feature メソッドの前に実行される |
def cleanupSpec() {} | 最後の feature メソッドの後に実行される |
feature メソッド
feature メソッドは以下の様になります。これは JUnit の @Test のテストメソッドに該当します。
ラベル付けされたブロック内に仕様の詳細を記述します。
def "pushing an element on the stack"() { setup: // ・・・ when: // ・・・ then: // ・・・ expect: // ・・・ cleanup: // ・・・ where: // ・・・ }
ブロックは以下の6種あり、ラベルに応じたブロックを含めます。
ブロックには以下のような項目を含めることができます(各ブロックの具体例は後述します)。
ラベル | 説明 |
---|---|
setup | feature で利用するオブジェクトなどの宣言と初期化を行うブロック。別名として given も同じ。 |
when | then にて示される予想結果の誘因となる任意のコードを含めることができる。 when-thenで繰り返して書くことができる。 |
then | when に応答する予想結果。条件、例外条件、相互作用、および変数の定義を含めることができる。 |
expect | 予想結果を定義。条件、変数の定義を含めることができる。when-thenと比べ関数的な記述となる。 |
cleanup | where ブロックに続けて定義し、feature メソッドのリソース解放などを書く。 |
where | データドリブンな feature の条件を書く。常に feature メソッドの最後に置く |
各ブロックには以下のようにドキュメントを含めることができます。
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 フォーマットで書くこともできます。
given: "an empty bank account" // ... when: "the account is credited $10" // ... then: "the account's balance is $10"
helper メソッド
feature メソッドが大きくなる場合や重複をさけるためにヘルパメソッドを定義できます。
def "offered PC matches preferred configuration"() { when: def pc = shop.buyPc() then: matchesPreferredConfiguration(pc) } void matchesPreferredConfiguration(pc) { assert pc.vendor == "Sunny" assert pc.clockRate >= 2333 assert pc.ram >= 4096 assert pc.os == "Linux" }
ヘルパメソッドは以下のように書くこともできます。
def matchesPreferredConfiguration(pc) { pc.vendor == "Sunny" && pc.clockRate >= 2333 && pc.ram >= 4096 && pc.os == "Linux" }
しかしこのようにするとテスト失敗時のレポートがわかりにくくなるため assert で記載することが推奨されています。
データドリブンなテスト
where による条件の指定は以下のようにテーブル形式(Data Tables)で書くことができます。
import spock.lang.* @Unroll class DataDrivenSpec extends Specification { def "minimum of #a and #b is #c"() { expect: Math.min(a, b) == c where: a | b || c 3 | 7 || 3 5 | 4 || 4 9 | 9 || 9 } }
上記例では結果を ||
を利用していますが、|
としても問題ありません。
変数が1つだけの場合でも|
は省略できないため以下のように書く必要があります。
a | _ 1 | _ 3 | _
@Unroll
@Unroll
は feature メソッド名に条件として使われた値を埋め込んだ形で外部に公開します。
指定しない場合は minimum of #a and #b is #c
というメソッド名となりますが、指定することで以下のように条件で与えられた名前が識別できるようになります(メソッドに個別に@Unroll
を付与することもできます)。
プロパティアクセスや引数なしのメソッドコールを含めることもできます。
def "#person is #person.age years old"() { ... } def "#person.name.toUpperCase()"() { ... }
data pipe
条件は以下のようにdata pipe(<<)で書くこともできます。
def "minimum of #a and #b is #c"() { expect: Math.min(a, b) == c where: a << [3, 5, 9] b << [7, 4, 9] c << [3, 4, 9] }
こんな書き方や [a, b, c] << sql.rows("select a, b, c from maxdata")
こんな書き方 [a, b, _, c] << sql.rows("select * from maxdata")
もできます。
条件には当然、以下のようにオブジェクトを初期化して渡すことができます。
def "#person.name is a #sex.toLowerCase() person"() { expect: person.getSex() == sex where: person || sex new Person(name: "Fred") || "Male" new Person(name: "Wilma") || "Female" }
Stack の仕様例
Stack の仕様の例を以下に示します。
import spock.lang.* class StackWithThreeElementsSpec extends Specification { def stack = new Stack() def setup() { ["elem1", "elem2", "elem3"].each { stack.push(it) } } def "size"() { expect: stack.size() == 3 } def "pop"() { expect: stack.pop() == "elem3" stack.pop() == "elem2" stack.pop() == "elem1" stack.size() == 0 } def "peek"() { expect: stack.peek() == "elem3" stack.peek() == "elem3" stack.size() == 3 } def "push"() { when: stack.push("elem4") then: stack.size() == 4 stack.peek() == "elem4" } }
フィールドで stack を初期化し、setup()
にて stack の fixture を定義しています。
各 feature で用意した fixture に対して stack 操作の検証を行っています。
最後の push のような手続き的な操作は when-then 形式で記載しています。なお、when-then のペアは何度でも繰り返して記載することができます。
実行結果は以下のようになります。
例外の検証
Stack が空の場合に pop 操作で例外となることを検証してみます。
class EmptyStackSpec extends Specification { def stack = new Stack() def "size"() { expect: stack.size() == 0 } def "pop"() { when: stack.pop() then: thrown(EmptyStackException) stack.empty } }
thrown(EmptyStackException)
にて例外が投げられることを検証できます。
例外の中身を調べたい場合は以下のようにします。
when: stack.pop() then: def e = thrown(EmptyStackException) e.cause == null
または以下のように取得することもできます。
when: stack.pop() then: EmptyStackException e = thrown() e.cause == null
例外とならないことの検証
HashMap はキーと値に null を許容します。
キーに null を与えた場合に例外とならないことを検証するには以下のようになります。
def "HashMap accepts null key"() { setup: def map = new HashMap() when: map.put(null, "elem") then: notThrown(NullPointerException) }
notThrown()
にて例外が投げられないという仕様を定義できます。
タイムアウトを適用する
タイムアウトをアノテーションで指定することができます。指定時間を超過する場合は失敗として扱われます。
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS) def "I better be quick" { ... }
クラス単位でSpecificationに指定することもできます。
@Timeout(10) class TimedSpec extends Specification { def "10秒超過でテスト失敗"() { ... } def "同様に10秒超過でテスト失敗"() { ... } @Timeout(value = 250, unit = MILLISECONDS) def "250ms超過でテスト失敗"() { ... } }
クラス単位で指定した場合も、個別の feature で定義を上書きできます。
Hamcrest matchers を使う
Hamcrest matchers を使うことで、より柔軟な条件の表現が可能となります。
import spock.lang.Specification import static spock.util.matcher.HamcrestMatchers.closeTo class HamcrestMatchersSpec extends Specification { def "comparing two decimal numbers"() { def myPi = 3.14 expect: myPi closeTo(Math.PI, 0.01) } }
<expected-value> <matcher>
の形式で条件を書けばよいだけです。
リソースのクリーンナップを行う
feature で利用するリソースのクリーンナップは以下のように行います。
setup: def file = new File("/some/path") file.createNewFile() // ... cleanup: file.delete()
AutoCleanup というアノテーションでライフタイムに応じた自動クリーンナップを行うこともできます。
@AutoCleanup
def foo
対象に @AutoCleanup を付与することで、デフォルトで close()
メソッドが呼び出されます。
異なるメソッドでクリーンナップを行いたい場合には、単にアノテーションでメソッド名を指定します。
@AutoCleanup("dispose") def foo
close処理に失敗した場合の例外を無視するには以下のように指定します。
@AutoCleanup(quiet = true) def foo
@Shared で共有リソースを利用する
feature 間で共有するリソースは @Shared でフィールド定義することで共有して利用できます。
以下の例はデータベース接続を共有して利用する例となります。
import groovy.sql.Sql import spock.lang.Shared import spock.lang.Specification class DatabaseDrivenSpec extends Specification { @Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver") def setupSpec() { sql.execute("create table maxdata (id int primary key, a int, b int, c int)") sql.execute("insert into maxdata values (1, 3, 7, 7), (2, 5, 4, 5), (3, 9, 9, 9)") } def "maximum of two numbers"() { expect: Math.max(a, b) == c where: [a, b, c] << sql.rows("select a, b, c from maxdata") } }
Specification に自然言語の名前をつける
Specification に対して自然言語で名前を適用する Title アノテーションが用意されています。
@Title("This is easy to read") class ThisIsHarderToReadSpec extends Specification { ... }
ヒアドキュメント形式で記載する場合には Narrative を使います。
@Narrative(""" As a user I want foo So that bar """) class GiveTheUserFooSpec() { ... }
MOPを適用する
spock.util.mop.Use
アノテーションでオブジェクトの振舞いの変更を適用できます。
class ListExtensions { static avg(List list) { list.sum() / list.size() } } class MySpec extends Specification { @Use(listExtensions) def "can use avg() method"() { expect: [1, 2, 3].avg() == 2 } }
List に対して avg() の振舞いを適用しています。
feature の実行を制限する
いくつかのアノテーションが提供されており、これらを使うことで feature の無効化などの制御ができます。
アノテーション | 説明 |
---|---|
@Ignore | 一時的な無効を設定。メソッドまたはクラス(仕様)に付与できる。 |
@IgnoreRest | 指定したメソッド以外を無効 |
@IgnoreIf | 指定した条件を満たす場合に無効化する |
@Requires | 指定する条件を満たす場合に有効化する |
@Stepwise | 失敗した以降の feature をスキップする |
@Ignore
feature の無効化。
@Ignore def "my feature"() { ... }
Specification 自体を無効化。
@Ignore class MySpec extends Specification { ... }
@IgnoreRest
def "無効化"() { ... } @IgnoreRest def "実行される"() { ... } def "これも無効化"() { ... }
@IgnoreIf
無効化する条件を指定できます。
@IgnoreIf({ System.getProperty("os.name").contains("windows") }) def "Windowsの場合は無効"() { ... }
クロージャ内では sys
env
os
jvm
というプロパティが使えるので上の例は以下のようにも書けます。
@IgnoreIf({ os.windows }) def "Windowsの場合は無効"() { ... }
@Requires
こちらは逆に有効化する条件を指定します。
@Requires({ os.windows }) def "Windowsの場合のみ実行"() { ... }
@Stepwise
失敗した以降の feature をスキップします。
@Stepwise class StepwiseExtensionSpec extends Specification { def "step 1"() { expect: true } def "step 2"() { expect: false } def "step 3"() { expect: true } }
step 2 が失敗となるため、step 3 がスキップされます。
次回は Spock の提供する Mocking API について見ていきます。 blog1.mammb.com
- 作者:Dierk Konig,Andrew Glover,Paul King,Guillaume Laforge,Jon Skeet
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2008/09/27
- メディア: 単行本(ソフトカバー)
Spock: Up and Running: Writing Expressive Tests in Java and Groovy
- 作者:Rob Fletcher
- 出版社/メーカー: O'Reilly Media
- 発売日: 2017/05/27
- メディア: ペーパーバック