圣杯生成所有'*'创建损坏的控制器测试?脚手架一代坏了吗?



更新3 -合格和不合格的例子

由于这篇文章的长度,所以我不会让我粘贴它们,所以例子可以在这里找到:http://pastebin.com/gMVa4Gd1

更新# 2。根据关于更新scaffolding插件版本的建议答案,我重新运行了generate-all '*'命令和测试。下面是更新后的输出,下面是生成的代码:

| Error Forked Grails VM exited with error
| Environment set to development.....
| Running without daemon...
| Running 2 unit tests...
| Running 2 unit tests... 1 of 2
| Running 2 unit tests... 2 of 2
| Running 2 unit tests... 3 of 3
| Failure:  Test the save action correctly persists an instance(AddressControllerSpec)
|  org.codehaus.groovy.grails.web.servlet.mvc.exceptions.CannotRedirectException: Cannot redirect for object [Address : (unsaved)] it is not a domain or has no identifier. Use an explicit redirect instead 
    at AddressController.tt__save_closure9_closure14(AddressController.groovy:43)
    at AddressController.$tt__save(AddressController.groovy:40)
    at AddressControllerSpec.Test the save action correctly persists an instance(AddressControllerSpec.groovy:54)
| Running 2 unit tests... 4 of 4
| Running 2 unit tests... 5 of 5
| Running 2 unit tests... 6 of 6
| Error 2014-06-06 09:39:45,246 [main] ERROR mvc.GrailsParameterMap  - Error processing form encoded PUT request
Message: null
   Line | Method
->>  98 | doCall                    in AddressController$_notFound_closure8_closure12
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|    96 | notFound                  in AddressController
|    56 | $tt__update . . . . . . . in     ''
|    98 | $spock_feature_0_5        in AddressControllerSpec
|   138 | invokeMethod . . . . . .  in org.spockframework.util.ReflectionUtil
|   330 | invokeRaw                 in org.spockframework.runtime.BaseSpecRunner
|   311 | invoke . . . . . . . . .  in     ''
|   285 | invokeFeatureMethod       in     ''
|   256 | doRunIteration . . . . .  in     ''
|   138 | invokeMethod              in org.spockframework.util.ReflectionUtil
|    91 | invokeTargetMethod . . .  in org.spockframework.runtime.extension.MethodInvocation
|    85 | proceed                   in     ''
|    37 | evaluate . . . . . . . .  in org.spockframework.runtime.extension.builtin.AbstractRuleInterceptor$1
|    48 | evaluate                  in grails.test.runtime.TestRuntimeJunitAdapter$1$2
|    38 | intercept . . . . . . . . in org.spockframework.runtime.extension.builtin.TestRuleInterceptor
|    84 | proceed                   in org.spockframework.runtime.extension.MethodInvocation
|   319 | invoke . . . . . . . . .  in org.spockframework.runtime.BaseSpecRunner
|   223 | runIteration              in     ''
|   214 | initializeAndRunIteration in     ''
|   205 | runSimpleFeature          in     ''
|   199 | doRunFeature . . . . . .  in     ''
|   138 | invokeMethod              in org.spockframework.util.ReflectionUtil
|   330 | invokeRaw . . . . . . . . in org.spockframework.runtime.BaseSpecRunner
|   311 | invoke                    in     ''
|   175 | runFeature . . . . . . .  in     ''
|   152 | runFeatures               in     ''
|   112 | doRunSpec . . . . . . . . in     ''
|   138 | invokeMethod              in org.spockframework.util.ReflectionUtil
|    91 | invokeTargetMethod . . .  in org.spockframework.runtime.extension.MethodInvocation
|    85 | proceed                   in     ''
|    37 | evaluate . . . . . . . .  in org.spockframework.runtime.extension.builtin.AbstractRuleInterceptor$1
|    74 | evaluate                  in grails.test.runtime.TestRuntimeJunitAdapter$3$4
|    38 | intercept . . . . . . . . in org.spockframework.runtime.extension.builtin.ClassRuleInterceptor
|    84 | proceed                   in org.spockframework.runtime.extension.MethodInvocation
|   319 | invoke . . . . . . . . .  in org.spockframework.runtime.BaseSpecRunner
|    91 | runSpec                   in     ''
|    82 | run . . . . . . . . . . . in     ''
|    63 | run                       in org.spockframework.runtime.Sputnik
|   127 | runChild . . . . . . . .  in org.junit.runners.Suite
|    26 | runChild                  in     ''
|   238 | run . . . . . . . . . . . in org.junit.runners.ParentRunner$3
|    63 | schedule                  in org.junit.runners.ParentRunner$1
|   236 | runChildren . . . . . . . in org.junit.runners.ParentRunner
|    53 | access$000                in     ''
|   229 | evaluate . . . . . . . .  in org.junit.runners.ParentRunner$2
|   309 | run                       in org.junit.runners.ParentRunner
|   160 | run . . . . . . . . . . . in org.junit.runner.JUnitCore
^   138 | run                       in     ''
| Failure:  Test the update action performs an update on a valid domain instance(AddressControllerSpec)
|  java.lang.NullPointerException: Cannot get property 'id' on null object
    at AddressControllerSpec.Test the update action performs an update on a valid domain instance(AddressControllerSpec.groovy:122)
| Running 2 unit tests... 7 of 7
| Failure:  Test that the delete action deletes an instance if it exists(AddressControllerSpec)
|  Condition not satisfied:
Address.count() == 1
        |       |
        0       false
    at AddressControllerSpec.Test that the delete action deletes an instance if it exists(AddressControllerSpec.groovy:142)
| Completed 7 unit tests, 3 failed in 0m 10s
Configuring Spring Security Core ...
... finished configuring Spring Security Core
| Tests FAILED 
| Error Forked Grails VM exited with error
/************************************* AddressControllerSpec ********************************************/

import grails.test.mixin.*
import spock.lang.*
@TestFor(AddressController)
@Mock(Address)
class AddressControllerSpec extends Specification {
    def populateValidParams(params) {
        assert params != null
        // TODO: Populate valid properties like...
        //params["name"] = 'someValidName'
    }
    void "Test the index action returns the correct model"() {
        when:"The index action is executed"
            controller.index()
        then:"The model is correct"
            !model.addressInstanceList
            model.addressInstanceCount == 0
    }
    void "Test the create action returns the correct model"() {
        when:"The create action is executed"
            controller.create()
        then:"The model is correctly created"
            model.addressInstance!= null
    }
    void "Test the save action correctly persists an instance"() {
        when:"The save action is executed with an invalid instance"
            request.contentType = FORM_CONTENT_TYPE
            request.method = 'POST'
            def address = new Address()
            address.validate()
            controller.save(address)
        then:"The create view is rendered again with the correct model"
            model.addressInstance!= null
            view == 'create'
        when:"The save action is executed with a valid instance"
            response.reset()
            populateValidParams(params)
            address = new Address(params)
            controller.save(address)
        then:"A redirect is issued to the show action"
            response.redirectedUrl == '/address/show/1'
            controller.flash.message != null
            Address.count() == 1
    }
    void "Test that the show action returns the correct model"() {
        when:"The show action is executed with a null domain"
            controller.show(null)
        then:"A 404 error is returned"
            response.status == 404
        when:"A domain instance is passed to the show action"
            populateValidParams(params)
            def address = new Address(params)
            controller.show(address)
        then:"A model is populated containing the domain instance"
            model.addressInstance == address
    }
    void "Test that the edit action returns the correct model"() {
        when:"The edit action is executed with a null domain"
            controller.edit(null)
        then:"A 404 error is returned"
            response.status == 404
        when:"A domain instance is passed to the edit action"
            populateValidParams(params)
            def address = new Address(params)
            controller.edit(address)
        then:"A model is populated containing the domain instance"
            model.addressInstance == address
    }
    void "Test the update action performs an update on a valid domain instance"() {
        when:"Update is called for a domain instance that doesn't exist"
            request.contentType = FORM_CONTENT_TYPE
            request.method = 'PUT'
            controller.update(null)
        then:"A 404 error is returned"
            response.redirectedUrl == '/address/index'
            flash.message != null

        when:"An invalid domain instance is passed to the update action"
            response.reset()
            def address = new Address()
            address.validate()
            controller.update(address)
        then:"The edit view is rendered again with the invalid instance"
            view == 'edit'
            model.addressInstance == address
        when:"A valid domain instance is passed to the update action"
            response.reset()
            populateValidParams(params)
            address = new Address(params).save(flush: true)
            controller.update(address)
        then:"A redirect is issues to the show action"
            response.redirectedUrl == "/address/show/$address.id"
            flash.message != null
    }
    void "Test that the delete action deletes an instance if it exists"() {
        when:"The delete action is called for a null instance"
            request.contentType = FORM_CONTENT_TYPE
            request.method = 'DELETE'
            controller.delete(null)
        then:"A 404 is returned"
            response.redirectedUrl == '/address/index'
            flash.message != null
        when:"A domain instance is created"
            response.reset()
            populateValidParams(params)
            def address = new Address(params).save(flush: true)
        then:"It exists"
            Address.count() == 1
        when:"The domain instance is passed to the delete action"
            controller.delete(address)
        then:"The instance is deleted"
            Address.count() == 0
            response.redirectedUrl == '/address/index'
            flash.message != null
    }
}

/******************************** AddressController ******************************************/


import static org.springframework.http.HttpStatus.*
import grails.transaction.Transactional
@Transactional(readOnly = true)
class AddressController {
    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
    def index(Integer max) {
        params.max = Math.min(max ?: 10, 100)
        respond Address.list(params), model:[addressInstanceCount: Address.count()]
    }
    def show(Address addressInstance) {
        respond addressInstance
    }
    def create() {
        respond new Address(params)
    }
    @Transactional
    def save(Address addressInstance) {
        if (addressInstance == null) {
            notFound()
            return
        }
        if (addressInstance.hasErrors()) {
            respond addressInstance.errors, view:'create'
            return
        }
        addressInstance.save flush:true
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.created.message', args: [message(code: 'address.label', default: 'Address'), addressInstance.id])
                redirect addressInstance
            }
            '*' { respond addressInstance, [status: CREATED] }
        }
    }
    def edit(Address addressInstance) {
        respond addressInstance
    }
    @Transactional
    def update(Address addressInstance) {
        if (addressInstance == null) {
            notFound()
            return
        }
        if (addressInstance.hasErrors()) {
            respond addressInstance.errors, view:'edit'
            return
        }
        addressInstance.save flush:true
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.updated.message', args: [message(code: 'Address.label', default: 'Address'), addressInstance.id])
                redirect addressInstance
            }
            '*'{ respond addressInstance, [status: OK] }
        }
    }
    @Transactional
    def delete(Address addressInstance) {
        if (addressInstance == null) {
            notFound()
            return
        }
        addressInstance.delete flush:true
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.deleted.message', args: [message(code: 'Address.label', default: 'Address'), addressInstance.id])
                redirect action:"index", method:"GET"
            }
            '*'{ render status: NO_CONTENT }
        }
    }
    protected void notFound() {
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.not.found.message', args: [message(code: 'address.label', default: 'Address'), params.id])
                redirect action: "index", method: "GET"
            }
            '*'{ render status: NOT_FOUND }
        }
    }
}

这似乎只在Intellij测试时发生。至少……当从命令行运行时,7个测试中只有3个失败。下面是命令行运行的结果:

test-app .AddressControllerSpec
| Running 2 unit tests... 3 of 3
| Failure:  Test the save action correctly persists an instance(.AddressControllerSpec)
|  Condition not satisfied:
model.addressInstance!= null
|     |              |
[:]   null           false
    at .AddressControllerSpec.Test the save action correctly persists an instance(AddressControllerSpec.groovy:48)
| Running 2 unit tests... 6 of 6
| Failure:  Test the update action performs an update on a valid domain instance(.AddressControllerSpec)
|  Condition not satisfied:
response.redirectedUrl == '/address/index'
|        |             |
|        null          false
org.codehaus.groovy.grails.plugins.testing.GrailsMockHttpServletResponse@2906a2bf
    at .AddressControllerSpec.Test the update action performs an update on a valid domain instance(AddressControllerSpec.groovy:102)
| Running 2 unit tests... 7 of 7
| Failure:  Test that the delete action deletes an instance if it exists(.AddressControllerSpec)
|  Condition not satisfied:
response.redirectedUrl == '/address/index'
|        |             |
|        null          false
org.codehaus.groovy.grails.plugins.testing.GrailsMockHttpServletResponse@590bd021
    at .AddressControllerSpec.Test that the delete action deletes an instance if it exists(AddressControllerSpec.groovy:133)
| Completed 7 unit tests, 3 failed in 0m 6s
Configuring Spring Security Core ...
... finished configuring Spring Security Core
| Tests FAILED 

在Grails 2.4.0中,我使用了" Grails generate-all '*'"来为我的应用程序生成脚手架。这也会创建控制器测试。然而,我认为这些控制器测试是错误的。在我的例子中,每次7个测试中有6个失败。下面是一个错误的例子:

@TestFor(AddressController)
@Mock(Address)
class AddressControllerSpec extends Specification {
.....
void "Test the create action returns the correct model"() {
        when:"The create action is executed"
            controller.create()
        then:"The model is correctly created"
            model.addressInstance != null
    }

当这个执行时,它失败了,输出是:

Condition not satisfied:
model.addressInstance != null
|     |               |
|     null            false
[address:Address : (unsaved)]
    at AddressControllerSpec.Test the create action returns the correct model(AddressControllerSpec.groovy:36)

注意模型本身是一个Address对象,但是addressInstance是空的。

查看生成的AddressController.groovy中的关联控制器动作:

def create() {
    respond new Address(params)
}

看到这没有设置一个名为addressInstance的变量,这不是很明显为什么这个测试失败吗?我是Grails的新手(好吧,尝试用它做一些事情的新手),所以我不确定是否有一些魔法应该在这里发挥作用,但事实是,模型确实被设置为由动作返回的对象,在我看来,这表明了一个bug。

谁能检查一下我的推理吗?我错了吗,这个代码是正确的?

唯一通过的测试是:"测试索引操作返回正确的模型"

作为参考,这里是完整的控制器和测试代码:
import static org.springframework.http.HttpStatus.*
import grails.transaction.Transactional
@Transactional(readOnly = true)
class AddressController {
    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
    def index(Integer max) {
        params.max = Math.min(max ?: 10, 100)
        respond Address.list(params), model:[addressInstanceCount: Address.count()]
    }
    def show(Address addressInstance) {
        respond addressInstance
    }
    def create() {
        respond new Address(params)
    }
    @Transactional
    def save(Address addressInstance) {
        if (addressInstance == null) {
            notFound()
            return
        }
        if (addressInstance.hasErrors()) {
            respond addressInstance.errors, view:'create'
            return
        }
        addressInstance.save flush:true
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.created.message', args: [message(code: 'address.label', default: 'Address'), addressInstance.id])
                redirect addressInstance
            }
            '*' { respond addressInstance, [status: CREATED] }
        }
    }
    def edit(Address addressInstance) {
        respond addressInstance
    }
    @Transactional
    def update(Address addressInstance) {
        if (addressInstance == null) {
            notFound()
            return
        }
        if (addressInstance.hasErrors()) {
            respond addressInstance.errors, view:'edit'
            return
        }
        addressInstance.save flush:true
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.updated.message', args: [message(code: 'Address.label', default: 'Address'), addressInstance.id])
                redirect addressInstance
            }
            '*'{ respond addressInstance, [status: OK] }
        }
    }
    @Transactional
    def delete(Address addressInstance) {
        if (addressInstance == null) {
            notFound()
            return
        }
        addressInstance.delete flush:true
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.deleted.message', args: [message(code: 'Address.label', default: 'Address'), addressInstance.id])
                redirect action:"index", method:"GET"
            }
            '*'{ render status: NO_CONTENT }
        }
    }
    protected void notFound() {
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.not.found.message', args: [message(code: 'address.label', default: 'Address'), params.id])
                redirect action: "index", method: "GET"
            }
            '*'{ render status: NOT_FOUND }
        }
    }
}

/*********************************************************************************/
import grails.test.mixin.*
import spock.lang.*

@TestFor(AddressController)
@Mock(Address)
class AddressControllerSpec extends Specification {
    def log = LogFactory.getLog(getClass())
    def populateValidParams(params) {
        assert params != null
        // TODO: Populate valid properties like...
        //params["name"] = 'someValidName'
    }
    void "Test the index action returns the correct model"() {
        when:"The index action is executed"
            controller.index()
        then:"The model is correct"
            !model.addressInstanceList
            model.addressInstanceCount == 0
    }
    void "Test the create action returns the correct model"() {
        when:"The create action is executed"
            controller.create()
        then:"The model is correctly created"
            model.addressInstance != null
    }
    void "Test the save action correctly persists an instance"() {
        when:"The save action is executed with an invalid instance"
            request.contentType = FORM_CONTENT_TYPE
            def address = new Address()
            address.validate()
            controller.save(address)
        then:"The create view is rendered again with the correct model"
            //model.addressInstance!= null
            view == 'create'
        when:"The save action is executed with a valid instance"
            response.reset()
            populateValidParams(params)
            address = new Address(params)
            controller.save(address)
        then:"A redirect is issued to the show action"
            response.redirectedUrl == '/address/show/1'
            controller.flash.message != null
            Address.count() == 1
    }
    void "Test that the show action returns the correct model"() {
        when:"The show action is executed with a null domain"
            controller.show(null)
        then:"A 404 error is returned"
            response.status == 404
        when:"A domain instance is passed to the show action"
            populateValidParams(params)
            def address = new Address(params)
            controller.show(address)
        then:"A model is populated containing the domain instance"
            model.addressInstance == address
    }
    void "Test that the edit action returns the correct model"() {
        when:"The edit action is executed with a null domain"
            controller.edit(null)
        then:"A 404 error is returned"
            response.status == 404
        when:"A domain instance is passed to the edit action"
            populateValidParams(params)
            def address = new Address(params)
            controller.edit(address)
        then:"A model is populated containing the domain instance"
            model.addressInstance == address
    }
    void "Test the update action performs an update on a valid domain instance"() {
        when:"Update is called for a domain instance that doesn't exist"
            request.contentType = FORM_CONTENT_TYPE
            controller.update(null)
        then:"A 404 error is returned"
            response.redirectedUrl == '/address/index'
            flash.message != null

        when:"An invalid domain instance is passed to the update action"
            response.reset()
            def address = new Address()
            address.validate()
            controller.update(address)
        then:"The edit view is rendered again with the invalid instance"
            view == 'edit'
            model.addressInstance == address
        when:"A valid domain instance is passed to the update action"
            response.reset()
            populateValidParams(params)
            address = new Address(params).save(flush: true)
            controller.update(address)
        then:"A redirect is issues to the show action"
            response.redirectedUrl == "/address/show/$address.id"
            flash.message != null
    }
    void "Test that the delete action deletes an instance if it exists"() {
        when:"The delete action is called for a null instance"
            request.contentType = FORM_CONTENT_TYPE
            controller.delete(null)
        then:"A 404 is returned"
            response.redirectedUrl == '/address/index'
            flash.message != null
        when:"A domain instance is created"
            response.reset()
            populateValidParams(params)
            def address = new Address(params).save(flush: true)
        then:"It exists"
            Address.count() == 1
        when:"The domain instance is passed to the delete action"
            controller.delete(address)
        then:"The instance is deleted"
            Address.count() == 0
            response.redirectedUrl == '/address/index'
            flash.message != null
    }
}

当测试由allowedMethods限制的控制器动作时,需要指定请求方法。

见https://jira.grails.org/browse/gpscaffold - 95

https://jira.grails.org/browse/grails - 8426

https://github.com/grails-plugins/grails-scaffolding/commit/db4eed57449e56225821ab565229b76bc394d2be

你可以在BuildConfig中更新脚手架插件。Groovy到2.1.1版本,或者您可以手动更新生成的测试。

最新更新