年末にGrailsのテストフィクスチャーも大掃除しよう #gadvent2012

Groovy Ecosystem

G* Advent Calendar 2012の14日目です。
13日目は@irofさんのGroovyでJUnitなテストを書くときの注意点……なんて無かったでした。

propertyMissingについて書くことをしいられているんだと思いましたが、ネタDSLを作ることぐらいしか思いつかなかったので予定していた内容を書かせて頂きます。

Grailsのテストコードにおいて、フィクスチャーの生成処理が各テストクラスにコピペ実装で散らばっているのを何とかしたいと思い、せっかくなのでBuild Test Data PluginGrails Fixtures Pluginを試してみることにしました。

お試し用のドメイン

よくあるAuthorドメインとBookドメインです。
両プラグインのドキュメントに載っていたものをほぼそのまま持って来ました。

package fixture.sandbox

class Author {

    String firstName
    String lastName

    static hasMany = [books: Book]

    static constraints = {
        firstName(blank: false)
        lastName(blank: false)
    }

    @Override
    String toString() {
        "$firstName $lastName"
    }

}
package fixture.sandbox

class Book {

    String title
    Date published
    BigDecimal price

    static belongsTo = [author: Author]

    static constraints = {
        title(blank: false)
        published(nullable: false)
        price(nullable: false, scale: 2, min: 5.95 as BigDecimal)
        author(nullable: false)
    }

    @Override
    String toString() {
        "$title published on $published by $author costs $$price"
    }

}

それといろふさんも必要ですよね。

package fixture.sandbox

class Irof {

    static final String IROF = 'irof'

    String name

    static constraints = {
        name(blank: false)
    }

    def propertyMissing(String name) {
        IROF
    }

    def propertyMissing(String name, value) {
        // すべてをのみこむのだ
    }

}

Build Test Data Plugin

まずはBuild Test Data Pluginについてです。

BuildConfig.groovypluginsセクションに以下を追加します。

compile ":build-test-data:2.0.3"

プラグインをインストールするとドメインに黒魔術がかけられてbuildメソッドが追加されます。buildメソッドを呼び出すと各フィールドにフィールド名が値として自動的に詰め込まれたドメインのインスタンスが生成されてDBへの保存までやってくれます。

package fixture.sandbox

import grails.plugin.spock.IntegrationSpec

class BuildTestDataSpec extends IntegrationSpec {

    def 'buildメソッドを単に呼び出してみる'() {
        when:
        def author = Author.build()

        then: 'デフォルトではフィールド名がそのまま値に入る'
        author.firstName == 'firstName'
        author.lastName == 'lastName'

        and: '永続化されてidとversionが払い出される'
        author.id != null
        author.version != null
    }

    def '永続化されるのでDBから取得できる'() {
        given: '元々存在していないこと'
        assert Author.findByFirstNameAndLastName('Guillaume', 'Laforge') == null

        when: 'buildメソッドを実行'
        Author.build(firstName: 'Guillaume', lastName: 'Laforge')

        and: 'DBから取得する'
        def guillaume = Author.findByFirstNameAndLastName('Guillaume', 'Laforge')

        then: 'データが取得できている'
        guillaume.firstName == 'Guillaume'
        guillaume.lastName == 'Laforge'
    }

    def '関連クラスも自動的に生成される'() {
        given: '元々存在していないこと'
        assert Book.list().size() == 0

        and:
        Author.list()*.delete()
        assert Author.list().size() == 0

        when: 'buildメソッドを実行'
        Book.build()

        then: 'データが生成されている'
        Book.list().size() == 1

        and: '関連クラスも生成されている'
        Author.list().size() == 1
    }

}

注意としてUnit Testの場合はテストクラスに@Buildアノテーションに対象ドメインのクラスを渡したものを付与する必要があります。

...
@Build(Author)
class AuthorControllerSpec extends UnitSpec {
...

複数の場合はリストで渡します。

...
@Build([Author, Book])
class BookControllerSpec extends UnitSpec {
...

このプラグインの嬉しいところはフィールドの値を設定して、大量のデータを生成することが容易にできることです。設定ファイルのテンプレートを次のコマンドを実行することで生成できます。

grails install-build-test-data-config-template

実行するとgrails-app/conf/TestDataConfig.groovyが作成されます。以下のようにカウンターを用意してクロージャーで食わせてあげると、buildされるたびにカウンターがインクリメントされながらデータが生成されていきます。

testDataConfig {
    sampleData {
        'fixture.sandbox.Irof' {
            def i = 1
            name = {-> "irof${i  }" }
        }
    }
}

テストで確認してみましょう。

package fixture.sandbox

import grails.plugin.spock.IntegrationSpec
import org.apache.commons.lang.RandomStringUtils

class CreateIrofSpec extends IntegrationSpec {

    def setupSpec() {
        (1000).times {
            Irof.build()
        }
    }

    def 'いろふさんが大量に生成されているはず'() {
        when: 'いろふさんをすべて取得'
        def irofs = Irof.list(sort: 'id')

        then: 'setupSpecで生成した分存在しているはず'
        irofs.size() == 1000

        and: 'TestDataConfigで設定した値が格納されているはず'
        def i = 1
        irofs*.name.every { it == "irof${i  }" }

        and: 'すべてはいろふに変わる'
        irofs*."${RandomStringUtils.randomAlphabetic(8)}".every { it == 'irof' }

        when: 'すべてはいろふに飲み込まれる'
        def irof = irofs.first()
        ['abcdefg', new Date(), new NullPointerException()].each {
            irof."${RandomStringUtils.randomAlphabetic(8)}" = it
        }

        then:
        notThrown(Exception)
    }

}

無事、いろふさんが大量に生成されましたね!

あと、productionモードで黒魔術が入り込まないよう、TestDataConfig.groovyに以下の内容を記述しておきましょう。テンプレート生成段階ではコメントアウトされています。

environments {
    production {
        testDataConfig {
            enabled = false
        }
    }
}

BuildConfig.groovyで制御してしまう手もあります。GrailsのWebサイト用のBuildConfig.groovyではこんな風に指定していました。

if (Environment.current == Environment.DEVELOPMENT) {
    compile ":build-test-data:1.1.1",
            ":fixtures:1.2"
}
else {
    test    ":build-test-data:1.1.1",
            ":fixtures:1.2"
}

Build Test Data Pluginについては、G*Magazine第5号杉浦さんが詳しく書かかれているのでそちらもご覧ください。

Grails Fixtures Plugin

続いてGrails Fixtures Pluginについてです。

BuildConfig.groovypluginsセクションに以下を追加します。

compile ":fixtures:1.2"

Grails Fixtures Pluginではフィクスチャーを個別のファイルで定義できます。デフォルトではGrailsプロジェクトの直下にfixturesディレクトリを掘って、その中にファイルを作成します。ここでは以下のファイルを作成します。

import fixture.sandbox.Author

fixture {
    // 記法その1
    dierk(Author, firstName: "Dierk", lastName: "König")

    // 記法その2
    guillaume(Author) {
        firstName = "Guillaume"
        lastName = "Laforge"
    }

    // 組み合わせもOK
    glen(Author, firstName: "Glen") {
        lastName = "Smith"
    }

    // 動的に生成することも可能ではある
    (1..10).each {
        "author${it}"(Author, firstName: "firstName${it}", lastName: "lastName${it}")
    }
}
import fixture.sandbox.Book

// 他の定義ファイルをincludeできる
include 'authors'

fixture {
    groovyInAction(Book) {
        title = 'Groovy in Action'
        published = '2007/01/10'
        price = 39.99
        author = dierk  // author.groovyで定義したAuthor
    }
    grailsInAction(Book) {
        title = 'Grails in Action'
        published = '2009/06/10'
        price = 35.99
        author = glen   // author.groovyで定義したAuthor
    }
}

// Build Test Data Pluginを組み合わせて使える
build {
    sampleBook(Book)
}

こんな感じで1つ1つに名前を付けて使います。Build Test Data Pluginはどちらかと言うと大量データ生成向けですが、こちらは特定条件の値を作りたいときに使うといいですね。

呼び出すときはfixtureLoaderにLoaderがInjectionされるのでそれを使います。上記で定義したファイル以外に、インラインでも定義できます。定義時の名前を使うときは、loadメソッドの返り値を受けておいて、プロパティとしてに定義した名前を指定すればインスタンスを取得できます。

package fixture.sandbox

import grails.plugin.spock.IntegrationSpec
import spock.lang.Shared

class GrailsFixturesSpec extends IntegrationSpec {

    @Shared
    def fixtureLoader

    @Shared
    def fixture

    def setupSpec() {
        // ファイルからロード
        fixture = fixtureLoader.load('books')

        // インラインでロード
        fixtureLoader.load {
            makingJavaGroovy(Book) {
                title = 'Making Java Groovy'
                published = '2013/05/01'
                price = 35.99
                author = ref('kenneth') // ref()で後ろに定義したものも参照可能
            }
            kenneth(Author) {
                firstName = 'Kenneth'
                lastName = 'Kousen'
            }
        }
    }

    def 'setupSpecでロードしたフィクスチャーが生成されること(ファイル版)'() {
        when:
        def groovyInAction = Book.findByTitle('Groovy in Action')

        then: '設定した通りに保存されている'
        groovyInAction.published == new Date('2007/01/10')
        groovyInAction.price == 39.99

        and: '関連も問題なし'
        groovyInAction.author.firstName == 'Dierk'
        groovyInAction.author.lastName == 'König'
    }

    def 'setupSpecでロードしたフィクスチャーが生成されること(インライン版)'() {
        when:
        def groovyInAction = Book.findByTitle('Making Java Groovy')

        then: '設定した通りに保存されている'
        groovyInAction.published == new Date('2013/05/01')
        groovyInAction.price == 35.99

        and: '関連も問題なし'
        groovyInAction.author.firstName == 'Kenneth'
        groovyInAction.author.lastName == 'Kousen'
    }

    def 'Build Test Data Pluginとの統合'() {
        expect: 'プラグインによるフィールド値の設定が行われている'
        fixture.sampleBook.title == 'title'
        fixture.sampleBook.price == 5.95
        fixture.sampleBook.published != null

        and: '関連も問題なし'
        fixture.sampleBook.author.firstName == 'firstName'
        fixture.sampleBook.author.lastName == 'lastName'

        and: '永続化されているのでidとversionが払い出されている'
        fixture.sampleBook.id != null
        fixture.sampleBook.version != null
    }

}

これらのプラグインを活用してフィクスチャーの整理をしてみたいと思います。工夫次第で美しく管理できそうですね。BootStrap.groovyや自前フィクスチャーが爆発している方はお試しください。序盤から使うとかなり楽だと思うので、新規に開発する場合はぜひ導入しておきたいですね。

15日目は@orange_clover(id:orangeclover)さんです。