[Grails]ユニットテスト入門（コントローラ編）

#参考
[http://grails.jp/doc/latest/guide/testing.html](http://grails.jp/doc/latest/guide/testing.html)
[http://grails.org/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`を実行すると、それ用のユニットテスト用ファイルが自動で生成される。

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

```groovy: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"() {
    }
}
```

```groovy: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"() {
    }
}

```


#コントローラテスト

```groovy:テスト対象コントローラの内容
package パッケージ名

import grails.converters.JSON
import grails.converters.XML
import grails.plugin.springsecurity.annotation.Secured
import org.springframework.web.multipart.MultipartFile

class TestController {

    def hello() {
        render "hello"
    }

    def redirectToHello() {
        redirect(controller: 'test', action: 'hello')
    }

    def methodPrinter() {
        render request.method
    }

    def home() {
        render(view: 'homePage', model: [title: 'Hello World'])
    }

    def showTemplate() {
        render(template:"snippet")
    }

    def showBookDetails() {
       // render(model: [title: 'The Nature Of Necessity', author: 'Alvin Plantinga'])
        [title: 'The Nature Of Necessity', author: 'Alvin Plantinga']
    }

    def renderXml() {
        render (contentType: 'text/xml') {
            book(title:"Great")
        }
    }

    def renderJson() {
        render(contentType: "application/json") {
            book = "Great"
        }
    }

    def xmlAndJsonRequest(TestCommand t) {
        render "Name is ${t.name}"

    }

    def xmlAndJsonRequestWithoutBinding(){
        request.withFormat {
            xml {
                // なぜかXMLが必ずnullになる。
                // Grailsのバグ？
                render "The XML Title Is ${request.XML}."
            }
            json {
                render "The JSON Title Is ${request.JSON.title}."
            }
        }
    }

    def mimeTypeHandling(){
        def data = [Hello:"World"]
        request.withFormat {
            xml { render data as XML}
            json { render data as JSON}
            html {data}
        }
    }

    // http://grails.org/doc/latest/ref/Controllers/withForm.html
    def formHandling() {
        withForm {
            render "Good"
        }.invalidToken {
            render "Bad"
        }
    }

    def uploadFile() {
        MultipartFile file = request.getFile("myFile")
        if(!file.isEmpty()) {
            file.transferTo( new File("/tmp/upload/${file.originalFilename}") )
            render "OK"
        } else {
            render "NG"
        }
    }

    def commandObjectHandling(TestCommand cmd) {
        if(cmd.hasErrors()) {
            render "Bad"
        } else {
            render "Good"
        }
    }

    static allowedMethods = [save:'POST']
    def save() {
        render "saved."
    }

    def showMessage() {
        render g.message(code:'foo.bar')
    }
}

class TestCommand {
    String name
    Integer age

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

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

import grails.test.mixin.TestFor
import org.codehaus.groovy.grails.plugins.testing.GrailsMockMultipartFile
import org.codehaus.groovy.grails.web.servlet.mvc.SynchronizerTokensHolder
import spock.lang.Specification

import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED
import static javax.servlet.http.HttpServletResponse.SC_OK


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

    def setup() {
    }

    def cleanup() {
    }

    void "基本的なテスト"() {
        when:
        controller.hello()

        then:
        response.text == 'hello'
    }

    void "リダイレクトのテスト"() {
        when:
        controller.redirectToHello()

        then:
        response.redirectedUrl == '/test/hello'
    }

    void "リクエストメソッドの確認"(){
        when:
        request.method ='POST'
        controller.methodPrinter()
        then:
        response.text == "POST"
    }

    void "リクエストメソッドの確認（複数バージョン）" () {
        setup:
        request.method = method
        controller.methodPrinter()

        expect:
        response.text == result

        where:
        method ||result
        'POST' || 'POST'
        'GET'  || 'GET'
    }

    void "表示されるviewと渡されるモデルのテスト" (){
        when:
        controller.home()

        then:
        view == '/test/homePage'
        model.title == 'Hello World'
    }

    void "Mapを返すアクションのテスト" () {
        // 上記のテストと違って、contrller側でmodelとわざわざ指定せずにデーてあ（Map)が返されている場合は
        // 以下のようにしてテストする必要がある。
        when:
        def model = controller.showBookDetails()

        then:
        model.author == 'Alvin Plantinga'
    }

    void "テンプレート表示テスト" () {
        // テンプレートの中身は直接レスポンスに書き込まれる。
        when:
        controller.showTemplate()

        then:
        response.text == "contents of template"
    }

    void "テストのためにテンプレートの中身を上書きすることもできる"() {
        // GroovyPages モックというらしい
        when:
        views ['/test/_snippet.gsp'] = 'mock template content'
        controller.showTemplate()

        then:
        response.text == 'mock template content'
    }



    void "XMLのレンダリングのテスト"() {
        // XMLもレスポンスに直接書き込まれる。
        // response.xmlはXmlSlurper
        when:
        controller.renderXml()

        then:
        response.text == "<book title='Great'/>"
        response.xml.@title.text() == 'Great'
    }

    void "JSONのレンダリングのテスト"() {
        // JSONもレスポンスに直接書き込まれる
        // response.jsonはorg.codehaus.groovy.grails.web.json.JSONElemen
        when:
        controller.renderJson()

        then:
        response.text == '{"book":"Great"}'
        response.json.book == 'Great'
    }


    void "xmlリクエストのテスト"() {
        when:
        request.xml = new Test(name: "koji", age: 29)
        controller.xmlAndJsonRequest()

        then:
        response.text == 'Name is koji'
    }

    void "jsonリクエストのテスト"() {
        when:
        request.json = new Test(name: "koji", age: 29)
        controller.xmlAndJsonRequest()

        then:
        response.text == 'Name is koji'
    }

// なんかしらんが動かない。
// 送信はちゃんと出来て、コントローラ側でもちゃんとXML扱いになるけど、何故かXMLとしての値が取れずにNULLになる。
//    void "xmlリクエストのテスト2"() {
//        when:
//            //request.xml = "<title>Wool</title>"
//            //request.xml = '<book title="The Stand"><title>aaaaaaaa</title></book>'
//            request.xml = new Test(name:"koji", age:29)
//            controller.xmlAndJsonRequestWithoutBinding()
//        then:
//            response.text == 'The XML Title Is Hoge!'
//    }

    void "jsonリクエストのテスト2"() {
        when:
        request.json = '{title:The Stand}'
        controller.xmlAndJsonRequestWithoutBinding()

        then:
        response.text == 'The JSON Title Is The Stand.'
    }

    void "マイムタイプの制御テスト（ｘｍｌ）"(){
        when:
        // XML_CONTENT_TYPEという定数もあるのでそいつを渡してもOK
        request.contentType = 'application/xml'
        controller.mimeTypeHandling()

        then:
        response.text == '<?xml version="1.0" encoding="UTF-8"?><map><entry key="Hello">World</entry></map>'
    }
    void "マイムタイプの制御テスト（json）"(){
        when:
        // JSON_CONTENT_TYPEという定数もあるのでそいつを渡してもOK
        request.contentType = 'application/json'
        controller.mimeTypeHandling()

        then:
        response.text == '{"Hello":"World"}'
    }

    void "フォームの2重送信テスト"() {
        // withForm を利用している場合、トークンが必須となる。
        when:
        controller.formHandling()

        then:
        response.text == "Bad"

        when:
        response.reset()
        def tokenHolder = SynchronizerTokensHolder.store(session)

        params[SynchronizerTokensHolder.TOKEN_URI] = '/test/formHandling'
        params[SynchronizerTokensHolder.TOKEN_KEY] = tokenHolder.generateToken(params[SynchronizerTokensHolder.TOKEN_URI])
        controller.formHandling()

        then:
        response.text == "Good"
    }

    void "ファイルアップロードテスト"() {
        when:
        GrailsMockMultipartFile file = new GrailsMockMultipartFile('myFile', 'test.jpg', 'image/jpeg', 'some file data'.bytes)
        request.addFile file
        controller.uploadFile()

        then:
        file.targetFileLocation.path == '/tmp/upload/test.jpg'
        response.text == "OK"
    }

    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'
    }

    void "allowedMethodsのテスト（saveメソッドはPOSTのみ許可とか）"() {
        setup:
        request.method = method
        controller.save()

        expect:
        response.status == result

        // SC_OKとかはjavax.servlet.http.HttpServletResponseクラスの定数
        where:
        method||result
        'post'|| SC_OK
        'get'|| SC_METHOD_NOT_ALLOWED
        'put'|| SC_METHOD_NOT_ALLOWED
        'update'|| SC_METHOD_NOT_ALLOWED
        'delete' || SC_METHOD_NOT_ALLOWED
    }

    void "タグライブラリg:messageの呼び出しテスト"() {
        when:
        // このテスト自体に特に意味は無いけど、messageSourceという機能を使えば動的にi18nの編集ができるっぽい
        messageSource.addMessage("foo.bar", request.locale, "Hello World")
        controller.showMessage()

        then:
        response.text == "Hello World"
    }

}
```