Update 3 - Passing and failing examples
SO won't let me paste them in because of the length of this post, so examples can be found here: http://pastebin.com/gMVa4Gd1
Update #2. Based on the proposed answer regarding updating the scaffolding plugin version, I've rerun the generate-all '*' command and the test. Here's the updated output, with the generated code beneath it:
| 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 }
}
}
}
Update
This appears to happen only when testing in Intellij. At least...only 3 of the 7 tests fail when run from the command line. Here're the results from a command line run:
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
In Grails 2.4.0, I've used "grails generate-all '*'" to generate scaffolding for my application. This also creates controller tests. However, I believe these controller tests are wrong. In my case, 6 out of 7 tests fail every time. Here's an example of a broken one:
@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
}
When this executes, it fails, and the output is:
Condition not satisfied:
model.addressInstance != null
| | |
| null false
[address:Address : (unsaved)]
at AddressControllerSpec.Test the create action returns the correct model(AddressControllerSpec.groovy:36)
Note that the model itself is an Address object, but addressInstance is null.
Looking at the associated controller action in generated AddressController.groovy:
def create() {
respond new Address(params)
}
Seeing how this doesn't set a variable called addressInstance, isn't this obviously why this test is failing? I'm new to Grails (well, new to trying to do something with it), so I'm not sure if there's some magic which is supposed to be at play here that isn't, but the fact that the model is indeed being set to the object returned by the action would seem to me to indicate a bug.
Can someone please check my reasoning here? Am I wrong about this code being in correct?
The only test which passes for me is: "Test the index action returns the correct model"
For reference, here's the full controller and test code:
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
}
}