7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Grails]ユニットテスト入門(ドメイン編)

Last updated at Posted at 2014-09-09

#参考
http://grails.jp/doc/latest/guide/testing.html
http://grails.org/doc/latest/guide/testing.html

はじめに

今回はGrailsのバージョンが2.4です。
標準でSpockでテストを書きます。
テストを実行するためには、以下のコマンドを実行します。

test-app もしくは test-app unit:

コレで実行可能なユニットテストが実行されます。
他にもオプションなどありますが、基本的に気にしないほうがいいです。テストが実行されなかったりして、混乱のもとになります。

現在自分がGrailsのユニットテストの書き方を勉強中です。
この記事はまだ完成していません。
Qiitaの下書きが埋まってしまったので、とりあえずの公開となります。
今後、内容の修正、追記を都度行います。

#豆知識
ユニットテストなので、Grailsは環境をtestとして、DataSource.groovyとBootstrap.groovyをユニットテストが終わった後に読み込ます。
コレは、ユニットテストを環境(データベース)に依存させないためです。
integrationテストが続いて実行される場合には、その情報が利用されます。

#テスト用ファイルの生成方法
create-domain-class Testとかcreate-controller Testコマンド、create-tag-lib Testを実行すると、それ用のユニットテスト用ファイルが自動で生成される。

デフォルトは以下のようなソースになっている。

TestSpec.groovy
package パッケージ名

import grails.test.mixin.TestFor
import spock.lang.Specification

/**
 * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions
 */
@TestFor(Test)
class TestSpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    void "test something"() {
    }
}
TestControllerSpec.groovy
package パッケージ名

import grails.test.mixin.TestFor
import spock.lang.Specification

/**
 * See the API for {@link grails.test.mixin.web.ControllerUnitTestMixin} for usage instructions
 */
@TestFor(TestController)
class TestControllerSpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    void "test something"() {
    }
}

コントローラで利用されるテストをしたい場合は、コントローラのテストクラスに@Mock(ドメイン名)と行った形でモック宣言しておく

#制約のテスト
制約のテストに関しては、4種類のタイプがある。
1.普通のドメインクラス
2.普通のGroovyクラスだけど、@grails.validation.Validateableが付いているクラス
3.コントローラ内に宣言されているコマンドオブジェクト
4.Config.groovyのgrails.validateable.classesに指定されたクラス。

基本的にドメインクラスのテストは、コントローラやサービス内で利用される際に、意図したデータが取得されるかどうかの確認がメインになるはずなので、本来はコントローラ内のテストでテストされるはず。

##ドメインクラスのテスト

テスト対象ドメイン
class Test {

    String name
    Integer age
    static constraints = {
        name()
        age(min: 0)
    }
    static mapping = {
        version(false)
    }
}
テストコード
package パッケージ名

import grails.test.mixin.TestFor
import spock.lang.Specification

/**
 * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions
 */
@TestFor(Test)
class TestSpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    void "基本的なテスト方法"() {
        when: 'ageが0未満'
        def p = new Test(name:'koji', age:-1)

        then: 'validationがコケるはず'
        !p.validate()

        when : 'ageが0以上'
        p = new Test(name:'koji', age:0)

        then:'validationが通るはず'
        p.validate()
    }
}

##コマンドオブジェクトのテスト
コントローラのテストとも言えるのかな?

テスト対象コントローラとコマンドオブジェクト
class TestController {
    def commandObjectHandling(TestCommand cmd) {
        if(cmd.hasErrors()) {
            render "Bad"
        } else {
            render "Good"
        }
    }
}
class TestCommand {
    String name
    Integer age

    static constraints = {
        name(blank:false, minSize: 1)
        age(min: 1)
    }
}

コントローラのテスト(テスト)
@TestFor(TestController)
class TestControllerSpec extends Specification {
    void "Commandオブジェクトのテスト"() {
        setup:
        def co = new TestCommand(name:name, age:age)
        // このテストの段階で、手動でvalidateしておくする必要がある。
        // というのも、このテストの段階で手動でコマンドオブジェクト(TestCommand)をnewしてcontrollerに渡しているので、
        // controller側ではコマンドオブジェクトのvalid()が実行されないため。
        co.validate()
        controller.commandObjectHandling(co)

        expect:
        response.text == result

        where:
        name  |age || result
        'koji'| 29 || 'Good'
        ''    | 29 || 'Bad'
    }

    void "Commandオブジェクトのテスト(バインディング)"() {
        setup:
        // こっちの場合、渡したプロパティをcontroller側で判断して自動的にコマンドオブジェクト(TestCommand)にバインディングしてくれて、
        // そのタイミングでvalidate()も裏で実行してくれているから、手動でvalidate()を実行する必要はない。
        params.name = name
        params.age = age
        controller.commandObjectHandling()

        expect:
        response.text == result

        where:
        name  |age || result
        'koji'| 29 || 'Good'
        ''    | 29 || 'Bad'
    }
}

この例ではTestCommandコマンドオブジェクトを含むコントローラ用のテストクラスからテストしているけど、create-unit-test TestCommandといった感じでTestCommandコマンドオブジェクトのテスト専用のユニットテストを用意するのもあり。
こんな感じ

コマンドオブジェクト用に専用テストを用意
package パッケージ名

import grails.test.mixin.TestMixin
import grails.test.mixin.support.GrailsUnitTestMixin
import spock.lang.Specification

/**
 * See the API for {@link grails.test.mixin.support.GrailsUnitTestMixin} for usage instructions
 */
@TestMixin(GrailsUnitTestMixin)
class TestCommandSpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    void "Commandオブジェクトのテスト"() {
        when:
        def co = new TestCommand(name:'koji', age:29)

        then:
        co.validate()
        !co.hasErrors()
        co.errors.errorCount == 0

        when:'エラーにする'
        co.age = -1

        then:
        !co.validate()
        co.hasErrors()
        co.errors.errorCount == 1
        co.errors['age'].code == 'min.notmet'

        when:'さらにエラーを増やす'
        co.name = ''

        then:
        !co.validate()
        co.hasErrors()
        co.errors.errorCount == 2
        co.errors['age'].code == 'min.notmet'
        co.errors['name'].code == 'blank'

        when:'最後に正常系をもう一回'
        co.name = 'koji'
        co.age = 29

        then:
        co.validate()
        !co.hasErrors()
    }
}

##普通のGroovyクラスのテスト
実際のGrailsアプリと違って、パッケージ宣言が無いとだめっぽい?
src/groovy配下に普通のGroovyクラスを作成

テスト対象の普通のGroovyクラス(専用アノテーション付き)
package mysource
@grails.validation.Validateable
class MyValidateable {
    String name
    Integer age

    static constraints = {
        name(nullable: false, blank: false, minSize: 1)
        age(range: 1..99)
    }
}

テスト用ファイルの生成
create-unit-test MyValidateable

テストコード
package パッケージ名

import grails.test.mixin.TestMixin
import grails.test.mixin.support.GrailsUnitTestMixin
import mysource.MyValidateable
import spock.lang.Specification

/**
 * See the API for {@link grails.test.mixin.support.GrailsUnitTestMixin} for usage instructions
 */
@TestMixin(GrailsUnitTestMixin)
class MyValidateableSpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    void "test something"() {
        when:
        def obj = new MyValidateable(name:'koji', age:29)

        then: 'no error!'
        obj.validate()
        !obj.hasErrors()
        obj.errors.errorCount == 0

        when:'エラーになるバージョン'
        obj = new MyValidateable(name:'', age:29)

        then: 'エラーの確認'
        !obj.validate()
        obj.hasErrors()
        obj.errors.errorCount == 1
        obj.errors['name'].code == 'blank'

        when: 'エラー情報をクリアすると?'
        obj.clearErrors()

        then: '当然こうなる。'
        !obj.hasErrors()
        obj.errors.errorCount == 0

        when: 'エラーにならないように修正すると?'
        obj.name = 'koji'

        then: '当然エラーにならない!'
        obj.validate()
        !obj.hasErrors()
        obj.errors.errorCount == 0

    }
}

Config.groovyで指定する場合

grails.validateable.classes = [com.demo.Book]という感じで指定するらしい。
テストの書き方は同じみたい。(未確認)

#その他(GORMの簡単な動作テスト)
ドメインクラスで、hasManyなどで値を持っているもののテストをどうしてもしたい場合は、@MockアノテーションにそのドメインをしておすればOK.

@TestFor(Test)
@Mock([Test2])
class TestSpec extends Specification {
    void "test"() {
        when:
        def p = new Test(name:'koji', age:29)
        p.addToTest2s(new Test2(name:"a"))
        p.addToTest2s(new Test2(name:"a"))
        p.save()

        def test2 = new Test2(name:"b").save()
        def p2 = new Test(name:'tarou',age:50)
        p2.addToTest2s(test2)
        p2.save()

        p.addToTest2s(test2)
        p.save(flush:true)

        then:
        p.test2s.size() == 3
        p2.test2s.size() == 1
        Test2.count == 3
    }
}

#所感
基本的に、ドメインでも普通のGroovyクラスでもコマンドオブジェクトでも、制約のテストはすべて基本的に同じ。(サンプルコードは別のことをやっているだけ)
ドメインの場合ダイナミックファインダーやらデータの保存やらがあるけど、それはGrails自体が用意している機能をそのまま利用するわけなので、ここの単体テストで自分でテストすることではないのかな。
あくまでコントローラやサービスで利用する際に、想定通りのデータが取得できる可動がのほうが大事だしテストしやすいと思う。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?