Grails におけるドメインクラスのユニットテストとインテグレーションテスト - Grails標準テストと Easyb プラグイン使用

前回 id:fits:20091101 の続きとして、今回はドメインクラスの関連をユニットテストする方法とインテグレーションテストの方法を紹介する。

関連のユニットテスト

関連をユニットテストする場合の注意点は、ドメインクラスの save メソッドを呼び出しても関連クラスには波及しないという点。

これは、GrailsUnitTestCase の mockDomain メソッド(実際は grails.test.MockUtils の addDynamicInstanceMethods メソッドで実施される)で追加される save メソッドのモックが関連クラスに対応してくれない事が原因。

そのため、ユニットテストでは関連クラスを別途 save したり、mockDomain に関連クラスのインスタンスを渡したりする事になる。

とりあえずドメインクラスでは、以下のように hasMany で subjects の関連を定義した。これにより、addToSubjects メソッドで Subject が複数追加できるようになる。

grails-app/domain/sample/User.groovy
package sample
/**
* テスト対象のドメインクラス
*/
class User {
    String name
    //subjects の型はデフォルトの HashSet をやめて List を指定
    List subjects
    static hasMany = [subjects: Subject]
    ・・・
}
grails-app/domain/sample/Subject.groovy
package sample
/**
* 関連クラス
*/
class Subject {
    String title
}

関連 subjects をテストするためのユニットテストは以下のようになる。Subject の save メソッドは本来不要だが、ユニットテストでは必要。

test/unit/sample/UserTests.groovy
package sample

import grails.test.*

class UserTests extends GrailsUnitTestCase {
    ・・・
    void testSubjects() {
        mockDomain(Subject)
        mockDomain(User)

        assertEquals 0, User.count()
        assertEquals 0, Subject.count()

        def subj = new Subject(title: "Grails のユニットテスト")
        //本来ここの save は不要だが、ユニットテストでは必要
        subj.save()

        def user = new User(name: "テスター")
        user.addToSubjects(subj)
        user.save()

        assertEquals 1, User.count()
        assertEquals 1, Subject.count()

        user.delete()

        assertEquals 0, User.count()
        //belongsTo は未定義のため、Subject は連動して削除されない
        assertEquals 1, Subject.count()
    }
}

Easyb 版は以下のようになる。

test/unit/sample/UserStory.groovy
scenario "User は Subject と1対多の関連", {
    given "'テスター' User オブジェクト", {
        mockDomain(Subject)
        mockDomain(User)

        subj = new Subject(title: "Grails のユニットテスト")
        user = new User(name: "テスター")
    }
    then "User を保存すると Subject を保存でき、User を削除しても Subject は残る", {
        User.count().shouldBe 0
        Subject.count().shouldBe 0

        //本来は不要だが、ユニットテストでは必要
        subj.save()

        user.addToSubjects(subj)
        user.save()

        User.count().shouldBe 1
        Subject.count().shouldBe 1

        user.delete()

        User.count().shouldBe 0
        //belongsTo は未定義のため、Subject は連動して削除されない
        Subject.count().shouldBe 1
    }
}

インテグレーションテスト

インテグレーションテストは、test/integration に配置し、ユニットテスト用のモックメソッド呼び出しが不要になる。(モックメソッドが要らないため GrailsUnitTestCase では無く、GroovyTestCase を継承すればよい)

Grails の標準テストの場合、インテグレーションクラス名とユニットテストクラス名が等しいと同一ファイルに結果が出力されるようなのでクラス名は別にしておいた方がよさそう。(Easyb 版は同一名でよい)

test/integration/sample/UserIntegrationTests.groovy
package sample

import groovy.util.*

class UserIntegrationTests extends GroovyTestCase {
//一応、以下のように GrailsUnitTestCase を継承していても動作する
//class UserIntegrationTests extends GrailsUnitTestCase {
    ・・・
    //subjects 関連のテスト
    void testSubjects() {
        assertEquals 0, User.count()
        assertEquals 0, Subject.count()

        def sub = new Subject(title: "Grails のテスト")
        sub.save()

        def user = new User(name: "テスター")
        user.addToSubjects(sub)
        user.save()

        assertEquals 1, User.count()
        assertEquals 1, Subject.count()

        user.delete()

        assertEquals 0, User.count()
        //belongsTo は未定義のため、Subject は連動して削除されない
        assertEquals 1, Subject.count()
    }
}

Easyb 版は以下の通り。grails-easyb-1.1 プラグインを使用する場合、delete の際に flush: true を指定しなければならないようなので注意。

test/integration/sample/UserStory.groovy
package sample
・・・
scenario "UserはSubjectと1対多の関連", {
    given "'テスター' User オブジェクト", {
        user = new User(name: "テスター")
    }
    then "User を保存すると Subject を保存でき、User を削除しても Subject は残る", {
        User.count().shouldBe 0
        Subject.count().shouldBe 0

        user.addToSubjects(new Subject(title: "Grails によるテスト"))
        user.save()

        User.count().shouldBe 1
        Subject.count().shouldBe 1

        def u = User.get(user.id)
        u.subjects.size().shouldBe 1
        u.subjects[0].title.shouldBe "Grails によるテスト"

        user.delete(flush: true)
        //以下のように flush: true を指定せずに delete すると User.count() が 0 にならない
        //user.delete()

        User.count().shouldBe 0
        //belongsTo は未定義のため、Subject は連動して削除されない
        Subject.count().shouldBe 1
    }
}