LoginSignup
4
4

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

コントローラテスト

テスト対象コントローラの内容
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)
    }
}
テストコード
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 "マイムタイプの制御テスト(xml)"(){
        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"
    }

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