You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ma...@apache.org on 2026/01/28 12:37:12 UTC
(grails-core) 01/01: test: suggestions for pr 15348
This is an automated email from the ASF dual-hosted git repository.
matrei pushed a commit to branch test/expand-integration-test-coverage-suggestions
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 8712675e4cfcb2f32281ece4aa65c846c0e1c446
Author: Mattias Reichel <ma...@gmail.com>
AuthorDate: Wed Jan 28 13:36:14 2026 +0100
test: suggestions for pr 15348
---
.../async/AsyncTestController.groovy | 37 +-
.../functionaltests/i18n/I18nTestController.groovy | 5 +-
.../ConditionalMatchInterceptor.groovy | 8 +-
.../interceptors/FirstInterceptor.groovy | 6 +-
.../interceptors/InterceptorTestController.groovy | 16 +-
.../interceptors/SecondInterceptor.groovy | 6 +-
.../interceptors/SessionInterceptor.groovy | 4 +-
.../interceptors/ThirdInterceptor.groovy | 6 +-
.../interceptors/TimingInterceptor.groovy | 4 +-
.../springevents/SpringEventController.groovy | 1 -
.../taglib/TagLibTestController.groovy | 4 +-
.../domain/functionaltests/binding/Employee.groovy | 1 -
.../functionaltests/constraints/PaymentInfo.groovy | 10 +-
.../caching/CacheTestService.groovy | 5 +-
.../services/AsyncProcessingService.groovy | 11 +-
.../functionaltests/taglib/CustomTagLib.groovy | 2 +-
.../functionaltests/async/AsyncPromiseSpec.groovy | 73 ++--
.../binding/AdvancedDataBindingSpec.groovy | 170 +-------
.../functionaltests/caching/CachingSpec.groovy | 107 ++---
.../codecs/SecurityCodecsSpec.groovy | 163 +------
.../commanddi/CommandObjectDISpec.groovy | 181 ++------
.../constraints/ConstraintValidationSpec.groovy | 9 +-
.../contentneg/ContentNegotiationSpec.groovy | 189 ++------
.../functionaltests/cors/CorsAdvancedSpec.groovy | 50 +--
.../errorhandling/ErrorHandlingSpec.groovy | 256 +++--------
.../functionaltests/events/DomainEventsSpec.groovy | 30 +-
.../fileupload/FileUploadSpec.groovy | 46 +-
.../functionaltests/gorm/GormAdvancedSpec.groovy | 17 +-
.../i18n/InternationalizationSpec.groovy | 187 +-------
.../interceptors/InterceptorOrderingSpec.groovy | 119 +----
.../requestresponse/RequestResponseSpec.groovy | 66 ++-
.../services/ServiceIntegrationSpec.groovy | 63 +--
.../springevents/SpringEventsSpec.groovy | 166 +++----
.../functionaltests/taglib/TagLibSpec.groovy | 477 +++------------------
.../urlmappings/UrlMappingsSpec.groovy | 182 ++------
.../services/pubsub/demo/TotalService.groovy | 8 +-
.../groovy/pubsub/demo/AsyncEventsSpec.groovy | 18 +-
.../groovy/pubsub/demo/PubSubSpec.groovy | 9 +-
.../main/groovy/pubsub/demo/BookSubscriber.groovy | 9 +-
.../gorm/grails-app/domain/gorm/Book.groovy | 2 -
.../groovy/gorm/FieldsValidationSpec.groovy | 113 ++---
.../groovy/gorm/GormCascadeOperationsSpec.groovy | 124 +++---
.../groovy/gorm/GormCriteriaQueriesSpec.groovy | 56 +--
.../groovy/gorm/GormDataServicesSpec.groovy | 101 ++---
.../groovy/gorm/GormEventsSpec.groovy | 108 ++---
.../groovy/gorm/ScaffoldingFunctionalSpec.groovy | 74 ++--
.../integration-test/groovy/gorm/UserSpec.groovy | 19 +-
.../groovy/gorm/ValidationConstraintsSpec.groovy | 53 +--
.../groovy/micronaut/MicronautContextSpec.groovy | 24 +-
.../groovy/micronaut/MicronautQualifierSpec.groovy | 12 +-
.../init/scaffoldingfields/Application.groovy | 3 +
.../init/scaffoldingfields/BootStrap.groovy | 153 +++----
.../scaffoldingfields/CustomTemplatesSpec.groovy | 20 +-
.../groovy/scaffoldingfields/FieldTypesSpec.groovy | 10 +-
.../groovy/scaffoldingfields/PaginationSpec.groovy | 94 ++--
.../RelationshipsFunctionalSpec.groovy | 88 ++--
.../ValidationFunctionalSpec.groovy | 66 ++-
.../scaffoldingfields/pages/EmployeePages.groovy | 3 +
58 files changed, 1235 insertions(+), 2609 deletions(-)
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/async/AsyncTestController.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/async/AsyncTestController.groovy
index 763062c3df..bb70cd9ca9 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/async/AsyncTestController.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/async/AsyncTestController.groovy
@@ -19,13 +19,14 @@
package functionaltests.async
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+
import functionaltests.services.AsyncProcessingService
-import grails.converters.JSON
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.TimeUnit
+import grails.converters.JSON
-import static grails.async.web.WebPromises.*
+import static grails.async.web.WebPromises.task
/**
* Controller demonstrating async request handling patterns in Grails.
@@ -125,13 +126,31 @@ class AsyncTestController {
def startTime = System.currentTimeMillis()
task {
- sleep delay
- return 'Completed in time'
- }.onComplete { result ->
+ def status = 'completed'
+ def message = 'Completed in time'
+ def completed = new AtomicBoolean(false)
+
+ def workThread = Thread.start {
+ sleep(delay)
+ completed.set(true)
+ }
+ workThread.join(timeout)
+
+ if (!completed.get()) {
+ workThread.interrupt()
+ status = 'timeout'
+ message = "Task exceeded timeout of ${timeout} ms"
+ }
+
+ return [
+ status: status,
+ message: message
+ ]
+ }.onComplete { taskResult ->
def elapsed = System.currentTimeMillis() - startTime
render([
- status: 'completed',
- result: result,
+ status: taskResult.status,
+ result: taskResult.message,
elapsedMs: elapsed
] as JSON)
}
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/i18n/I18nTestController.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/i18n/I18nTestController.groovy
index 25fc947264..1695314b0d 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/i18n/I18nTestController.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/i18n/I18nTestController.groovy
@@ -19,11 +19,11 @@
package functionaltests.i18n
-import grails.converters.JSON
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.web.servlet.LocaleResolver
-import org.springframework.web.servlet.support.RequestContextUtils
+
+import grails.converters.JSON
/**
* Controller for testing internationalization (i18n) features.
@@ -32,7 +32,6 @@ import org.springframework.web.servlet.support.RequestContextUtils
class I18nTestController {
MessageSource messageSource
- LocaleResolver localeResolver
/**
* Get a simple message using the current locale.
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ConditionalMatchInterceptor.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ConditionalMatchInterceptor.groovy
index e72f29c009..74d049cac3 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ConditionalMatchInterceptor.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ConditionalMatchInterceptor.groovy
@@ -32,10 +32,10 @@ class ConditionalMatchInterceptor {
boolean before() {
// Only add to execution order if 'match' param is 'yes'
if (params.match == 'yes') {
- InterceptorTestController.executionOrder << "conditional:before:matched"
+ InterceptorTestController.executionOrder << 'conditional:before:matched'
request.setAttribute('conditionalMatched', true)
} else {
- InterceptorTestController.executionOrder << "conditional:before:notmatched"
+ InterceptorTestController.executionOrder << 'conditional:before:notmatched'
request.setAttribute('conditionalMatched', false)
}
true
@@ -43,9 +43,9 @@ class ConditionalMatchInterceptor {
boolean after() {
if (params.match == 'yes') {
- InterceptorTestController.executionOrder << "conditional:after:matched"
+ InterceptorTestController.executionOrder << 'conditional:after:matched'
} else {
- InterceptorTestController.executionOrder << "conditional:after:notmatched"
+ InterceptorTestController.executionOrder << 'conditional:after:notmatched'
}
true
}
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/FirstInterceptor.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/FirstInterceptor.groovy
index ddba8a9cef..ad51f493aa 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/FirstInterceptor.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/FirstInterceptor.groovy
@@ -32,17 +32,17 @@ class FirstInterceptor {
}
boolean before() {
- InterceptorTestController.executionOrder << "first:before"
+ InterceptorTestController.executionOrder << 'first:before'
request.setAttribute('firstInterceptorRan', true)
true
}
boolean after() {
- InterceptorTestController.executionOrder << "first:after"
+ InterceptorTestController.executionOrder << 'first:after'
true
}
void afterView() {
- InterceptorTestController.executionOrder << "first:afterView"
+ InterceptorTestController.executionOrder << 'first:afterView'
}
}
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/InterceptorTestController.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/InterceptorTestController.groovy
index 3f520f68d9..322b23cd55 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/InterceptorTestController.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/InterceptorTestController.groovy
@@ -33,7 +33,7 @@ class InterceptorTestController {
* Simple action for testing interceptor before/after execution.
*/
def index() {
- executionOrder << "controller:index"
+ executionOrder << 'controller:index'
render([
action: 'index',
executionOrder: new ArrayList<>(executionOrder)
@@ -44,7 +44,7 @@ class InterceptorTestController {
* Action for testing interceptor ordering.
*/
def testOrder() {
- executionOrder << "controller:testOrder"
+ executionOrder << 'controller:testOrder'
render([
action: 'testOrder',
executionOrder: new ArrayList<>(executionOrder)
@@ -55,7 +55,7 @@ class InterceptorTestController {
* Action that can be blocked by interceptor.
*/
def blocked() {
- executionOrder << "controller:blocked"
+ executionOrder << 'controller:blocked'
render([
action: 'blocked',
message: 'This should not be seen if blocked'
@@ -79,7 +79,7 @@ class InterceptorTestController {
* Action for testing model modification by interceptor.
*/
def modifyModel() {
- executionOrder << "controller:modifyModel"
+ executionOrder << 'controller:modifyModel'
[
originalValue: 'from controller',
timestamp: System.currentTimeMillis()
@@ -90,7 +90,7 @@ class InterceptorTestController {
* Action that returns data to be potentially modified by after interceptor.
*/
def dataAction() {
- executionOrder << "controller:dataAction"
+ executionOrder << 'controller:dataAction'
render([
data: params.data ?: 'default',
interceptorModified: false
@@ -142,7 +142,7 @@ class InterceptorTestController {
* Action for testing afterView interceptor.
*/
def withView() {
- executionOrder << "controller:withView"
+ executionOrder << 'controller:withView'
[message: 'Hello from controller']
}
@@ -152,7 +152,7 @@ class InterceptorTestController {
def slowAction() {
def delay = params.int('delay') ?: 100
Thread.sleep(delay)
- executionOrder << "controller:slowAction"
+ executionOrder << 'controller:slowAction'
render([
action: 'slowAction',
delay: delay
@@ -163,7 +163,7 @@ class InterceptorTestController {
* Action for testing conditional interceptor matching.
*/
def conditionalAction() {
- executionOrder << "controller:conditionalAction"
+ executionOrder << 'controller:conditionalAction'
render([
action: 'conditionalAction',
param: params.match
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SecondInterceptor.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SecondInterceptor.groovy
index f6c70cfa74..f3cb409386 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SecondInterceptor.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SecondInterceptor.groovy
@@ -32,17 +32,17 @@ class SecondInterceptor {
}
boolean before() {
- InterceptorTestController.executionOrder << "second:before"
+ InterceptorTestController.executionOrder << 'second:before'
request.setAttribute('secondInterceptorRan', true)
true
}
boolean after() {
- InterceptorTestController.executionOrder << "second:after"
+ InterceptorTestController.executionOrder << 'second:after'
true
}
void afterView() {
- InterceptorTestController.executionOrder << "second:afterView"
+ InterceptorTestController.executionOrder << 'second:afterView'
}
}
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SessionInterceptor.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SessionInterceptor.groovy
index 129d85ad90..bc20ad8585 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SessionInterceptor.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SessionInterceptor.groovy
@@ -29,7 +29,7 @@ class SessionInterceptor {
}
boolean before() {
- InterceptorTestController.executionOrder << "session:before"
+ InterceptorTestController.executionOrder << 'session:before'
session.setAttribute('interceptorSessionData', [
setAt: System.currentTimeMillis(),
message: 'Session data from interceptor'
@@ -38,7 +38,7 @@ class SessionInterceptor {
}
boolean after() {
- InterceptorTestController.executionOrder << "session:after"
+ InterceptorTestController.executionOrder << 'session:after'
true
}
}
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ThirdInterceptor.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ThirdInterceptor.groovy
index 8bdbbaa47e..dd1435796a 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ThirdInterceptor.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ThirdInterceptor.groovy
@@ -32,17 +32,17 @@ class ThirdInterceptor {
}
boolean before() {
- InterceptorTestController.executionOrder << "third:before"
+ InterceptorTestController.executionOrder << 'third:before'
request.setAttribute('thirdInterceptorRan', true)
true
}
boolean after() {
- InterceptorTestController.executionOrder << "third:after"
+ InterceptorTestController.executionOrder << 'third:after'
true
}
void afterView() {
- InterceptorTestController.executionOrder << "third:afterView"
+ InterceptorTestController.executionOrder << 'third:afterView'
}
}
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/TimingInterceptor.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/TimingInterceptor.groovy
index 8a4c2311e4..bd0896255e 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/TimingInterceptor.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/TimingInterceptor.groovy
@@ -29,7 +29,7 @@ class TimingInterceptor {
}
boolean before() {
- InterceptorTestController.executionOrder << "timing:before"
+ InterceptorTestController.executionOrder << 'timing:before'
request.setAttribute('requestStartTime', System.currentTimeMillis())
true
}
@@ -38,7 +38,7 @@ class TimingInterceptor {
def startTime = request.getAttribute('requestStartTime') as Long
def endTime = System.currentTimeMillis()
def duration = endTime - startTime
- InterceptorTestController.executionOrder << "timing:after:${duration}ms"
+ InterceptorTestController.executionOrder << "timing:after:${duration}ms".toString()
// Add header without checking if committed
response.addHeader('X-Request-Duration', "${duration}")
true
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/springevents/SpringEventController.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/springevents/SpringEventController.groovy
index e4f6368aa2..860860f674 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/springevents/SpringEventController.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/springevents/SpringEventController.groovy
@@ -20,7 +20,6 @@
package functionaltests.springevents
import grails.converters.JSON
-import org.springframework.context.ApplicationEventPublisher
import org.springframework.beans.factory.annotation.Autowired
/**
diff --git a/grails-test-examples/app1/grails-app/controllers/functionaltests/taglib/TagLibTestController.groovy b/grails-test-examples/app1/grails-app/controllers/functionaltests/taglib/TagLibTestController.groovy
index 95c7068fff..9166397a2e 100644
--- a/grails-test-examples/app1/grails-app/controllers/functionaltests/taglib/TagLibTestController.groovy
+++ b/grails-test-examples/app1/grails-app/controllers/functionaltests/taglib/TagLibTestController.groovy
@@ -19,6 +19,8 @@
package functionaltests.taglib
+import jakarta.servlet.http.Cookie
+
/**
* Controller for testing GSP Tag Libraries.
* Provides actions that render GSP views using both custom and built-in tags.
@@ -217,7 +219,7 @@ class TagLibTestController {
def testBuiltInCookie() {
if (params.setCookie) {
- response.addCookie(new jakarta.servlet.http.Cookie('testCookie', params.setCookie))
+ response.addCookie(new Cookie('testCookie', params.setCookie))
}
[cookieName: params.cookieName ?: 'testCookie', defaultValue: params.default ?: 'no cookie']
}
diff --git a/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Employee.groovy b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Employee.groovy
index 2fc5e27d96..4238823a12 100644
--- a/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Employee.groovy
+++ b/grails-test-examples/app1/grails-app/domain/functionaltests/binding/Employee.groovy
@@ -19,7 +19,6 @@
package functionaltests.binding
-import grails.databinding.BindInitializer
import grails.databinding.BindUsing
import grails.databinding.BindingFormat
diff --git a/grails-test-examples/app1/grails-app/domain/functionaltests/constraints/PaymentInfo.groovy b/grails-test-examples/app1/grails-app/domain/functionaltests/constraints/PaymentInfo.groovy
index c3ac70396a..4474f2c437 100644
--- a/grails-test-examples/app1/grails-app/domain/functionaltests/constraints/PaymentInfo.groovy
+++ b/grails-test-examples/app1/grails-app/domain/functionaltests/constraints/PaymentInfo.groovy
@@ -19,6 +19,8 @@
package functionaltests.constraints
+import java.math.RoundingMode
+
/**
* Domain class demonstrating financial constraints:
* - Credit card validation
@@ -123,8 +125,8 @@ class PaymentInfo {
// Tax amount must match calculation
taxAmount nullable: true, scale: 2, validator: { val, obj ->
if (val == null || obj.amount == null || obj.taxRate == null) return true
- def expectedTax = (obj.amount * obj.taxRate / 100).setScale(2, BigDecimal.ROUND_HALF_UP)
- if (val.setScale(2, BigDecimal.ROUND_HALF_UP) != expectedTax) {
+ def expectedTax = (obj.amount * obj.taxRate / 100).setScale(2, RoundingMode.HALF_UP)
+ if (val.setScale(2, RoundingMode.HALF_UP) != expectedTax) {
return 'paymentInfo.taxAmount.doesNotMatchCalculation'
}
}
@@ -132,8 +134,8 @@ class PaymentInfo {
// Total must equal amount + tax
totalAmount scale: 2, validator: { val, obj ->
if (val == null || obj.amount == null) return true
- def expectedTotal = obj.amount + (obj.taxAmount ?: 0)
- if (val.setScale(2, BigDecimal.ROUND_HALF_UP) != expectedTotal.setScale(2, BigDecimal.ROUND_HALF_UP)) {
+ def expectedTotal = obj.amount + (obj.taxAmount ?: 0) as BigDecimal
+ if (val.setScale(2, RoundingMode.HALF_UP) != expectedTotal.setScale(2, RoundingMode.HALF_UP)) {
return 'paymentInfo.totalAmount.doesNotMatchSum'
}
}
diff --git a/grails-test-examples/app1/grails-app/services/functionaltests/caching/CacheTestService.groovy b/grails-test-examples/app1/grails-app/services/functionaltests/caching/CacheTestService.groovy
index 841a0c65e7..37736dcbe9 100644
--- a/grails-test-examples/app1/grails-app/services/functionaltests/caching/CacheTestService.groovy
+++ b/grails-test-examples/app1/grails-app/services/functionaltests/caching/CacheTestService.groovy
@@ -19,10 +19,11 @@
package functionaltests.caching
-import grails.plugin.cache.Cacheable
+import groovy.transform.CompileStatic
+
import grails.plugin.cache.CacheEvict
import grails.plugin.cache.CachePut
-import groovy.transform.CompileStatic
+import grails.plugin.cache.Cacheable
/**
* Service demonstrating Grails caching features.
diff --git a/grails-test-examples/app1/grails-app/services/functionaltests/services/AsyncProcessingService.groovy b/grails-test-examples/app1/grails-app/services/functionaltests/services/AsyncProcessingService.groovy
index 0da8268862..4eeb3da9e9 100644
--- a/grails-test-examples/app1/grails-app/services/functionaltests/services/AsyncProcessingService.groovy
+++ b/grails-test-examples/app1/grails-app/services/functionaltests/services/AsyncProcessingService.groovy
@@ -16,16 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.services
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Future
+
+import groovy.util.logging.Slf4j
+
import org.springframework.scheduling.annotation.Async
import org.springframework.scheduling.annotation.AsyncResult
-import grails.gorm.transactions.Transactional
-import groovy.util.logging.Slf4j
-import java.util.concurrent.Future
-import java.util.concurrent.CompletableFuture
+import grails.gorm.transactions.Transactional
/**
* Service demonstrating async operations.
diff --git a/grails-test-examples/app1/grails-app/taglib/functionaltests/taglib/CustomTagLib.groovy b/grails-test-examples/app1/grails-app/taglib/functionaltests/taglib/CustomTagLib.groovy
index f4a3ee820b..532017139d 100644
--- a/grails-test-examples/app1/grails-app/taglib/functionaltests/taglib/CustomTagLib.groovy
+++ b/grails-test-examples/app1/grails-app/taglib/functionaltests/taglib/CustomTagLib.groovy
@@ -25,7 +25,7 @@ package functionaltests.taglib
*/
class CustomTagLib {
- static namespace = "custom"
+ static namespace = 'custom'
// Use 'none' encoding for tags that output HTML structure
// Individual tags can use encodeAsHTML() where needed
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
index 9d5b028330..43f3feb750 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
@@ -19,27 +19,26 @@
package functionaltests.async
-import functionaltests.Application
-import functionaltests.services.AsyncProcessingService
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
+import java.util.concurrent.TimeUnit
+
import groovy.json.JsonSlurper
+
+import functionaltests.services.AsyncProcessingService
import io.micronaut.http.HttpRequest
-import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
-import spock.lang.Specification
import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
-import java.util.concurrent.TimeUnit
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for async/promise functionality in Grails.
* Tests various async patterns including tasks, promises, chaining, and error handling.
*/
-@Integration(applicationClass = Application)
-@Rollback
+@Integration
class AsyncPromiseSpec extends Specification {
@Shared
@@ -48,18 +47,18 @@ class AsyncPromiseSpec extends Specification {
AsyncProcessingService asyncProcessingService
def setup() {
- client = HttpClient.create(new URL("http://localhost:${serverPort}"))
+ client = client ?: HttpClient.create(new URL("http://localhost:${serverPort}"))
}
- def cleanup() {
- client?.close()
+ def cleanupSpec() {
+ client.close()
}
// ========== Basic Async Task Tests ==========
def "simple async task completes successfully"() {
when: "calling simple task endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/asyncTest/simpleTask'),
String
)
@@ -76,7 +75,7 @@ class AsyncPromiseSpec extends Specification {
def value = 7
when: "calling compute endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET("/asyncTest/computeTask?value=${value}"),
String
)
@@ -90,7 +89,7 @@ class AsyncPromiseSpec extends Specification {
def "parallel tasks complete and return all results"() {
when: "calling parallel tasks endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/asyncTest/parallelTasks'),
String
)
@@ -110,7 +109,7 @@ class AsyncPromiseSpec extends Specification {
def input = 'hello'
when: "calling chained endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET("/asyncTest/chainedTasks?input=${input}"),
String
)
@@ -127,7 +126,7 @@ class AsyncPromiseSpec extends Specification {
def "async task handles success without error"() {
when: "calling task that should succeed"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/asyncTest/taskWithError?fail=false'),
String
)
@@ -139,19 +138,25 @@ class AsyncPromiseSpec extends Specification {
json.result == 'Success'
}
- def "async task with timeout completes within time limit"() {
+ @Unroll
+ def "async task with timeout completes within time limit"(int delay, int timeout, int elapsedMin, String status, String result) {
when: "calling task with reasonable timeout"
- HttpResponse<String> response = client.toBlocking().exchange(
- HttpRequest.GET('/asyncTest/taskWithTimeout?delay=100&timeout=500'),
+ def response = client.toBlocking().exchange(
+ HttpRequest.GET("/asyncTest/taskWithTimeout?delay=$delay&timeout=$timeout"),
String
)
def json = new JsonSlurper().parseText(response.body())
- then: "task completes successfully"
+ then: "task completes as expected"
response.status == HttpStatus.OK
- json.status == 'completed'
- json.result == 'Completed in time'
- json.elapsedMs >= 100
+ json.status == status
+ json.result.startsWith(result)
+ json.elapsedMs >= elapsedMin
+
+ where:
+ delay | timeout | elapsedMin | status | result
+ 100 | 500 | 100 | 'completed' | 'Completed in time'
+ 100 | 10 | 10 | 'timeout' | 'Task exceeded timeout of '
}
// ========== Async Service Tests ==========
@@ -161,7 +166,7 @@ class AsyncPromiseSpec extends Specification {
def input = 'test'
when: "calling async service endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET("/asyncTest/useAsyncService?input=${input}"),
String
)
@@ -178,7 +183,7 @@ class AsyncPromiseSpec extends Specification {
def value = 6
when: "calling async calculation endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET("/asyncTest/asyncCalculation?value=${value}"),
String
)
@@ -192,7 +197,7 @@ class AsyncPromiseSpec extends Specification {
def "async batch processing reverses all items"() {
when: "calling batch endpoint with default items"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/asyncTest/asyncBatch'),
String
)
@@ -211,7 +216,7 @@ class AsyncPromiseSpec extends Specification {
def taskId = 'task-123'
when: "calling long running endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET("/asyncTest/longRunning?taskId=${taskId}"),
String
)
@@ -231,7 +236,7 @@ class AsyncPromiseSpec extends Specification {
def v2 = 4
when: "calling compose endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET("/asyncTest/composeFutures?v1=${v1}&v2=${v2}"),
String
)
@@ -251,7 +256,7 @@ class AsyncPromiseSpec extends Specification {
def body = '{"name": "test", "value": "hello"}'
when: "posting to process endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/asyncTest/processRequestData', body)
.contentType(MediaType.APPLICATION_JSON),
String
@@ -267,7 +272,7 @@ class AsyncPromiseSpec extends Specification {
def "multi-stage process reports all stages"() {
when: "calling multi-stage endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/asyncTest/multiStageProcess'),
String
)
@@ -285,7 +290,7 @@ class AsyncPromiseSpec extends Specification {
def "stages execute in correct order"() {
when: "calling multi-stage endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/asyncTest/multiStageProcess'),
String
)
@@ -305,7 +310,7 @@ class AsyncPromiseSpec extends Specification {
def input = 'grails'
when: "calling with async=true"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET("/asyncTest/conditionalAsync?async=true&input=${input}"),
String
)
@@ -323,7 +328,7 @@ class AsyncPromiseSpec extends Specification {
def input = 'grails'
when: "calling with async=false"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET("/asyncTest/conditionalAsync?async=false&input=${input}"),
String
)
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy
index 7fb441b62c..b3e3777ab9 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/binding/AdvancedDataBindingSpec.groovy
@@ -16,17 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.binding
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import io.micronaut.http.HttpRequest
-import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
+import spock.lang.Shared
import spock.lang.Specification
+import grails.testing.mixin.integration.Integration
+
/**
* Comprehensive integration tests for advanced data binding features.
*
@@ -45,19 +44,23 @@ import spock.lang.Specification
* - Empty string to null conversion
* - String trimming
*/
-@Integration(applicationClass = Application)
+@Integration
class AdvancedDataBindingSpec extends Specification {
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
+ @Shared
+ HttpClient client
+
+ def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
+ }
+
+ def cleanupSpec() {
+ client.close()
}
// ========== Map-Based Binding Tests ==========
def "test basic map-based binding"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=John&lastName=Doe&salary=50000'),
@@ -69,15 +72,9 @@ class AdvancedDataBindingSpec extends Specification {
response.body().firstName == 'John'
response.body().lastName == 'Doe'
response.body().salary == 50000
-
- cleanup:
- client.close()
}
def "test nested object binding"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=Jane&homeAddress.street=123+Main+St&homeAddress.city=Springfield&homeAddress.state=IL'),
@@ -91,17 +88,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().homeAddress.street == '123 Main St'
response.body().homeAddress.city == 'Springfield'
response.body().homeAddress.state == 'IL'
-
- cleanup:
- client.close()
}
// ========== @BindUsing Annotation Tests ==========
def "test @BindUsing annotation lowercases and trims email"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithBindUsing?email=John.Doe%40Example.COM'),
@@ -112,15 +103,9 @@ class AdvancedDataBindingSpec extends Specification {
response.status.code == 200
response.body().email == 'john.doe@example.com'
response.body().originalEmail == 'John.Doe@Example.COM'
-
- cleanup:
- client.close()
}
def "test @BindUsing with mixed case email"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithBindUsing?email=TEST.User%40DOMAIN.org'),
@@ -130,17 +115,11 @@ class AdvancedDataBindingSpec extends Specification {
then:
response.status.code == 200
response.body().email == 'test.user@domain.org'
-
- cleanup:
- client.close()
}
// ========== @BindingFormat Annotation Tests ==========
def "test @BindingFormat for date parsing - MMddyyyy format"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithDateFormat?hireDate=01152020'),
@@ -151,15 +130,9 @@ class AdvancedDataBindingSpec extends Specification {
response.status.code == 200
response.body().hireDate == '2020-01-15'
response.body().hireDateInput == '01152020'
-
- cleanup:
- client.close()
}
def "test @BindingFormat for date parsing - yyyy-MM-dd format"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithDateFormat?birthDate=1990-05-20'),
@@ -170,15 +143,9 @@ class AdvancedDataBindingSpec extends Specification {
response.status.code == 200
response.body().birthDate == '1990-05-20'
response.body().birthDateInput == '1990-05-20'
-
- cleanup:
- client.close()
}
def "test multiple date formats in same request"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithDateFormat?hireDate=03012021&birthDate=1985-12-25'),
@@ -189,17 +156,11 @@ class AdvancedDataBindingSpec extends Specification {
response.status.code == 200
response.body().hireDate == '2021-03-01'
response.body().birthDate == '1985-12-25'
-
- cleanup:
- client.close()
}
// ========== Collection Binding Tests (List) ==========
def "test binding to List collection"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindTeamWithMembers?name=Engineering&members%5B0%5D.name=Alice&members%5B0%5D.role=Lead&members%5B1%5D.name=Bob&members%5B1%5D.role=Developer'),
@@ -214,15 +175,9 @@ class AdvancedDataBindingSpec extends Specification {
response.body().members[0].role == 'Lead'
response.body().members[1].name == 'Bob'
response.body().members[1].role == 'Developer'
-
- cleanup:
- client.close()
}
def "test binding to List with gaps in indices"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindTeamWithMembers?name=QA&members%5B0%5D.name=Carol&members%5B2%5D.name=Dave'),
@@ -236,17 +191,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().members.size() == 2
response.body().members.find { it.name == 'Carol' } != null
response.body().members.find { it.name == 'Dave' } != null
-
- cleanup:
- client.close()
}
// ========== Map Collection Binding Tests ==========
def "test binding to Map collection"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindProjectWithContributors?name=GrailsCore&contributors%5Blead%5D.name=John&contributors%5Blead%5D.expertise=Architecture&contributors%5Bdev%5D.name=Jane&contributors%5Bdev%5D.expertise=Testing'),
@@ -260,17 +209,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().contributors.lead.expertise == 'Architecture'
response.body().contributors.dev.name == 'Jane'
response.body().contributors.dev.expertise == 'Testing'
-
- cleanup:
- client.close()
}
// ========== @RequestParameter Annotation Tests ==========
def "test @RequestParameter maps different parameter names"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithRequestParameter?firstName=Robert&lastName=Smith&age=30'),
@@ -282,17 +225,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().givenName == 'Robert'
response.body().familyName == 'Smith'
response.body().age == 30
-
- cleanup:
- client.close()
}
// ========== bindData with Include/Exclude Tests ==========
def "test bindData with include - only specified properties bound"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithIncludeExclude?firstName=Test&lastName=User&email=test%40example.com&salary=100000'),
@@ -305,17 +242,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().lastName == 'User'
response.body().email == null
response.body().salary == null
-
- cleanup:
- client.close()
}
// ========== Selective Property Binding Tests ==========
def "test selective property binding using subscript operator"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindSelectiveProperties?firstName=Selective&lastName=Test&email=should.not.bind%40test.com&salary=999'),
@@ -328,17 +259,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().lastName == 'Test'
response.body().email == null
response.body().salary == null
-
- cleanup:
- client.close()
}
// ========== Direct Data Binder Usage Tests ==========
def "test using grailsWebDataBinder directly"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindUsingDirectBinder?firstName=Direct&lastName=Binder&email=DIRECT%40TEST.COM'),
@@ -351,17 +276,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().lastName == 'Binder'
// Email should be lowercased due to @BindUsing
response.body().email == 'direct@test.com'
-
- cleanup:
- client.close()
}
// ========== Command Object Binding Tests ==========
def "test command object binding with validation - valid data"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindCommandObject?firstName=Valid&lastName=User&email=valid%40email.com'),
@@ -375,15 +294,9 @@ class AdvancedDataBindingSpec extends Specification {
response.body().email == 'valid@email.com'
response.body().valid == true
response.body().errors.isEmpty()
-
- cleanup:
- client.close()
}
def "test command object binding with validation - invalid data"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindCommandObject?firstName=&lastName=&email=invalid-email'),
@@ -395,15 +308,9 @@ class AdvancedDataBindingSpec extends Specification {
response.body().valid == false
response.body().errors.contains('firstName')
response.body().errors.contains('lastName')
-
- cleanup:
- client.close()
}
def "test nested command object binding"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindNestedCommandObject?name=Contact+Person&address.street=456+Oak+Ave&address.city=Portland'),
@@ -415,17 +322,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().name == 'Contact Person'
response.body().address.street == '456 Oak Ave'
response.body().address.city == 'Portland'
-
- cleanup:
- client.close()
}
// ========== JSON Body Binding Tests ==========
def "test JSON body binding to command object"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.POST('/advancedDataBinding/bindJsonBody', [
@@ -442,17 +343,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().lastName == 'JsonLast'
response.body().email == 'json@test.com'
response.body().valid == true
-
- cleanup:
- client.close()
}
// ========== Multiple Command Objects Tests ==========
def "test binding multiple command objects"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindMultipleCommandObjects?employee.firstName=Multi&employee.lastName=Test&address.street=789+Pine+Rd&address.city=Seattle'),
@@ -465,17 +360,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().employee.lastName == 'Test'
response.body().address.street == '789 Pine Rd'
response.body().address.city == 'Seattle'
-
- cleanup:
- client.close()
}
// ========== Empty String Conversion Tests ==========
def "test empty string converts to null"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindEmptyStrings?firstName=&lastName=HasValue'),
@@ -487,17 +376,11 @@ class AdvancedDataBindingSpec extends Specification {
response.body().firstNameIsNull == true
response.body().lastName == 'HasValue'
response.body().lastNameIsNull == false
-
- cleanup:
- client.close()
}
// ========== String Trimming Tests ==========
def "test string trimming during binding"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithTrimming?firstName=+++Trimmed+++'),
@@ -508,17 +391,11 @@ class AdvancedDataBindingSpec extends Specification {
response.status.code == 200
response.body().firstName == 'Trimmed'
response.body().firstNameLength == 7
-
- cleanup:
- client.close()
}
// ========== Type Conversion Tests ==========
def "test valid type conversion"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindWithTypeConversion?salary=75000&firstName=TypeTest'),
@@ -529,17 +406,11 @@ class AdvancedDataBindingSpec extends Specification {
response.status.code == 200
response.body().salary == 75000
response.body().firstName == 'TypeTest'
-
- cleanup:
- client.close()
}
// ========== Edge Cases ==========
def "test binding with special characters in values"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=O%27Brien&lastName=M%C3%BCller'),
@@ -550,15 +421,9 @@ class AdvancedDataBindingSpec extends Specification {
response.status.code == 200
response.body().firstName == "O'Brien"
response.body().lastName == 'Müller'
-
- cleanup:
- client.close()
}
def "test binding with unicode characters"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=%E6%97%A5%E6%9C%AC%E8%AA%9E'),
@@ -568,15 +433,9 @@ class AdvancedDataBindingSpec extends Specification {
then:
response.status.code == 200
response.body().firstName == '日本語'
-
- cleanup:
- client.close()
}
def "test binding with null parameter values"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/advancedDataBinding/bindEmployee?firstName=TestNull'),
@@ -588,8 +447,5 @@ class AdvancedDataBindingSpec extends Specification {
response.body().firstName == 'TestNull'
response.body().lastName == null
response.body().homeAddress == null
-
- cleanup:
- client.close()
}
}
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/caching/CachingSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/caching/CachingSpec.groovy
index 695c4902a6..3ab4ad36c0 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/caching/CachingSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/caching/CachingSpec.groovy
@@ -19,17 +19,19 @@
package functionaltests.caching
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import groovy.json.JsonSlurper
+
import io.micronaut.http.HttpRequest
-import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
-import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Narrative
+import spock.lang.Shared
import spock.lang.Specification
+import org.springframework.beans.factory.annotation.Autowired
+
+import grails.testing.mixin.integration.Integration
+
/**
* Integration tests for Grails caching with @Cacheable, @CacheEvict, @CachePut.
*
@@ -40,7 +42,7 @@ import spock.lang.Specification
* counting method invocations, since @Cacheable proxies prevent method body
* execution on cache hits.
*/
-@Integration(applicationClass = Application)
+@Integration
@Narrative('''
Grails caching provides method-level caching via annotations @Cacheable,
@CacheEvict, and @CachePut. This allows expensive operations to be cached
@@ -51,17 +53,22 @@ class CachingSpec extends Specification {
@Autowired
CacheTestService cacheTestService
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
- }
+ @Shared
+ HttpClient client
def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
+
// Evict all caches before each test to ensure clean state
cacheTestService.evictBasicCache()
cacheTestService.evictAllFromParamCache()
cacheTestService.evictAllKeyedCache()
}
+ def cleanupSpec() {
+ client.close()
+ }
+
// ========== Basic @Cacheable Tests - Data Consistency ==========
def "cached method returns same result on subsequent calls"() {
@@ -71,7 +78,7 @@ class CachingSpec extends Specification {
then: "same result is returned (proving caching works)"
result1 == result2
- result1.startsWith("Basic data:")
+ result1.startsWith('Basic data:')
}
def "cached method returns different result after eviction"() {
@@ -86,8 +93,8 @@ class CachingSpec extends Specification {
then: "new result is generated (timestamps differ)"
result1 != result2
- result1.startsWith("Basic data:")
- result2.startsWith("Basic data:")
+ result1.startsWith('Basic data:')
+ result2.startsWith('Basic data:')
}
def "multiple eviction and fetch cycles work correctly"() {
@@ -208,13 +215,13 @@ class CachingSpec extends Specification {
def "CachePut updates cache with new value using key closure"() {
given: "existing cached value for a key"
def original = cacheTestService.getByKey('mykey')
- original.startsWith("Value for mykey:")
+ original.startsWith('Value for mykey:')
when: "updating cache with new value"
- def updated = cacheTestService.updateByKey('mykey', "New value for mykey")
+ def updated = cacheTestService.updateByKey('mykey', 'New value for mykey')
then: "updated value is returned"
- updated == "New value for mykey"
+ updated == 'New value for mykey'
when: "getting by key again"
def afterUpdate = cacheTestService.getByKey('mykey')
@@ -225,25 +232,25 @@ class CachingSpec extends Specification {
def "CachePut can be called multiple times"() {
when: "updating cache multiple times"
- cacheTestService.updateByKey('testkey', "Value 1")
- cacheTestService.updateByKey('testkey', "Value 2")
- def finalValue = cacheTestService.updateByKey('testkey', "Value 3")
+ cacheTestService.updateByKey('testkey', 'Value 1')
+ cacheTestService.updateByKey('testkey', 'Value 2')
+ def finalValue = cacheTestService.updateByKey('testkey', 'Value 3')
then: "last update wins"
- finalValue == "Value 3"
- cacheTestService.getByKey('testkey') == "Value 3"
+ finalValue == 'Value 3'
+ cacheTestService.getByKey('testkey') == 'Value 3'
}
def "CachePut for one key does not affect other keys"() {
given: "two different cached keys"
- def key1Original = cacheTestService.getByKey('key1')
+ cacheTestService.getByKey('key1')
def key2Original = cacheTestService.getByKey('key2')
when: "updating only key1"
- cacheTestService.updateByKey('key1', "Updated key1")
+ cacheTestService.updateByKey('key1', 'Updated key1')
then: "key1 is updated, key2 is unchanged"
- cacheTestService.getByKey('key1') == "Updated key1"
+ cacheTestService.getByKey('key1') == 'Updated key1'
cacheTestService.getByKey('key2') == key2Original
}
@@ -272,18 +279,15 @@ class CachingSpec extends Specification {
// ========== HTTP Endpoint Tests ==========
def "basic cache works via HTTP"() {
- given:
- def client = createClient()
-
// Evict cache to start fresh
client.toBlocking().exchange(HttpRequest.GET('/cacheTest/evictBasic'), String)
when: "calling endpoint twice"
- HttpResponse<String> response1 = client.toBlocking().exchange(
+ def response1 = client.toBlocking().exchange(
HttpRequest.GET('/cacheTest/basicData'),
String
)
- HttpResponse<String> response2 = client.toBlocking().exchange(
+ def response2 = client.toBlocking().exchange(
HttpRequest.GET('/cacheTest/basicData'),
String
)
@@ -296,24 +300,18 @@ class CachingSpec extends Specification {
def json2 = new JsonSlurper().parseText(response2.body())
json1.data == json2.data
-
- cleanup:
- client.close()
}
def "parameter cache works via HTTP"() {
- given:
- def client = createClient()
-
// Evict cache
client.toBlocking().exchange(HttpRequest.GET('/cacheTest/evictAllParam'), String)
when: "calling with same ID twice"
- HttpResponse<String> response1 = client.toBlocking().exchange(
+ def response1 = client.toBlocking().exchange(
HttpRequest.GET('/cacheTest/dataById?id=42'),
String
)
- HttpResponse<String> response2 = client.toBlocking().exchange(
+ def response2 = client.toBlocking().exchange(
HttpRequest.GET('/cacheTest/dataById?id=42'),
String
)
@@ -323,24 +321,21 @@ class CachingSpec extends Specification {
def json2 = new JsonSlurper().parseText(response2.body())
json1.data == json2.data
-
- cleanup:
- client.close()
}
def "eviction works via HTTP"() {
- given:
- def client = createClient()
-
// Evict cache and populate it
client.toBlocking().exchange(HttpRequest.GET('/cacheTest/evictBasic'), String)
- def firstCall = client.toBlocking().exchange(HttpRequest.GET('/cacheTest/basicData'), String)
+ def firstCall = client.toBlocking().exchange(
+ HttpRequest.GET('/cacheTest/basicData'),
+ String
+ )
def firstData = new JsonSlurper().parseText(firstCall.body()).data
when: "evicting via HTTP then calling again after delay"
client.toBlocking().exchange(HttpRequest.GET('/cacheTest/evictBasic'), String)
Thread.sleep(10) // Ensure timestamp changes
- HttpResponse<String> afterEvict = client.toBlocking().exchange(
+ def afterEvict = client.toBlocking().exchange(
HttpRequest.GET('/cacheTest/basicData'),
String
)
@@ -348,14 +343,10 @@ class CachingSpec extends Specification {
then: "new data generated after eviction"
def afterEvictData = new JsonSlurper().parseText(afterEvict.body()).data
firstData != afterEvictData
-
- cleanup:
- client.close()
}
def "different IDs return different cached values via HTTP"() {
given:
- def client = createClient()
client.toBlocking().exchange(HttpRequest.GET('/cacheTest/evictAllParam'), String)
when: "fetching different IDs"
@@ -370,15 +361,9 @@ class CachingSpec extends Specification {
json1.data != json2.data
json1.data == json3.data
-
- cleanup:
- client.close()
}
def "complex data endpoint works with caching"() {
- given:
- def client = createClient()
-
when: "fetching complex data"
def response1 = client.toBlocking().exchange(
HttpRequest.GET('/cacheTest/complexData?category=electronics&page=3'),
@@ -396,15 +381,9 @@ class CachingSpec extends Specification {
json1.data == json2.data
json1.data.category == 'electronics'
json1.data.page == 3
-
- cleanup:
- client.close()
}
def "CachePut works via HTTP with key closure"() {
- given:
- def client = createClient()
-
// Evict keyed cache and get initial value
client.toBlocking().exchange(HttpRequest.GET('/cacheTest/evictAllKeyed'), String)
def initial = new JsonSlurper().parseText(
@@ -412,16 +391,16 @@ class CachingSpec extends Specification {
).data
when: "updating via HTTP"
- client.toBlocking().exchange(HttpRequest.GET('/cacheTest/updateByKey?key=httpkey&value=HTTPUpdated'), String)
+ client.toBlocking().exchange(
+ HttpRequest.GET('/cacheTest/updateByKey?key=httpkey&value=HTTPUpdated'),
+ String
+ )
def afterUpdate = new JsonSlurper().parseText(
client.toBlocking().exchange(HttpRequest.GET('/cacheTest/byKey?key=httpkey'), String).body()
).data
then: "cache contains updated value"
- afterUpdate == "HTTPUpdated"
+ afterUpdate == 'HTTPUpdated'
afterUpdate != initial
-
- cleanup:
- client.close()
}
}
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/codecs/SecurityCodecsSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/codecs/SecurityCodecsSpec.groovy
index 80879971af..ad19ab280e 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/codecs/SecurityCodecsSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/codecs/SecurityCodecsSpec.groovy
@@ -16,15 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.codecs
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
+import spock.lang.Shared
import spock.lang.Specification
+import grails.testing.mixin.integration.Integration
+
/**
* Comprehensive integration tests for Grails codec functionality.
*
@@ -39,19 +39,23 @@ import spock.lang.Specification
* - Edge cases (null, empty, special characters)
* - Hash consistency verification
*/
-@Integration(applicationClass = Application)
+@Integration
class SecurityCodecsSpec extends Specification {
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
+ @Shared
+ HttpClient client
+
+ def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
+ }
+
+ def cleanupSpec() {
+ client.close()
}
// ========== HTML Encoding Tests (XSS Prevention) ==========
def "test HTML encoding escapes dangerous tags"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeHtml?input=%3Cscript%3Ealert(%22XSS%22)%3C/script%3E'),
@@ -65,15 +69,9 @@ class SecurityCodecsSpec extends Specification {
response.body().encoded.contains('</script>')
!response.body().encoded.contains('<script>')
response.body().decodedBack == '<script>alert("XSS")</script>'
-
- cleanup:
- client.close()
}
def "test HTML encoding escapes quotes"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeHtml?input=%22quoted%22'),
@@ -84,15 +82,9 @@ class SecurityCodecsSpec extends Specification {
response.status.code == 200
response.body().encoded.contains('"')
response.body().decodedBack == '"quoted"'
-
- cleanup:
- client.close()
}
def "test HTML encoding escapes ampersands"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeHtml?input=foo%26bar'),
@@ -104,17 +96,11 @@ class SecurityCodecsSpec extends Specification {
response.body().input == 'foo&bar'
response.body().encoded.contains('&')
response.body().decodedBack == 'foo&bar'
-
- cleanup:
- client.close()
}
// ========== URL Encoding Tests ==========
def "test URL encoding escapes spaces and special chars"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeUrl?input=hello+world%26foo%3Dbar'),
@@ -128,17 +114,11 @@ class SecurityCodecsSpec extends Specification {
response.body().encoded.contains('%26')
response.body().encoded.contains('%3D')
response.body().decodedBack == 'hello world&foo=bar'
-
- cleanup:
- client.close()
}
// ========== Base64 Encoding Tests ==========
def "test Base64 encoding and decoding text"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeBase64?input=Hello%2C+World!'),
@@ -150,15 +130,9 @@ class SecurityCodecsSpec extends Specification {
response.body().input == 'Hello, World!'
response.body().encoded == 'SGVsbG8sIFdvcmxkIQ=='
response.body().decodedBack == 'Hello, World!'
-
- cleanup:
- client.close()
}
def "test Base64 encoding with binary data"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeBase64Binary'),
@@ -170,17 +144,11 @@ class SecurityCodecsSpec extends Specification {
response.body().originalBytes == [72, 101, 108, 108, 111] // "Hello" in ASCII
response.body().encoded == 'SGVsbG8='
response.body().decodedBytes == [72, 101, 108, 108, 111]
-
- cleanup:
- client.close()
}
// ========== MD5 Hash Tests ==========
def "test MD5 hashing produces consistent 32-char hex string"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeMd5?input=password123'),
@@ -192,15 +160,9 @@ class SecurityCodecsSpec extends Specification {
response.body().input == 'password123'
response.body().hashLength == 32
response.body().md5Hash ==~ /^[a-f0-9]{32}$/
-
- cleanup:
- client.close()
}
def "test MD5 bytes produces 16 bytes"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeMd5Bytes?input=password123'),
@@ -211,17 +173,11 @@ class SecurityCodecsSpec extends Specification {
response.status.code == 200
response.body().bytesLength == 16
response.body().md5Bytes.size() == 16
-
- cleanup:
- client.close()
}
// ========== SHA1 Hash Tests ==========
def "test SHA1 hashing produces consistent 40-char hex string"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeSha1?input=password123'),
@@ -232,15 +188,9 @@ class SecurityCodecsSpec extends Specification {
response.status.code == 200
response.body().hashLength == 40
response.body().sha1Hash ==~ /^[a-f0-9]{40}$/
-
- cleanup:
- client.close()
}
def "test SHA1 bytes produces 20 bytes"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeSha1Bytes?input=password123'),
@@ -250,17 +200,11 @@ class SecurityCodecsSpec extends Specification {
then: "SHA1 bytes should be 20 bytes"
response.status.code == 200
response.body().bytesLength == 20
-
- cleanup:
- client.close()
}
// ========== SHA256 Hash Tests ==========
def "test SHA256 hashing produces consistent 64-char hex string"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeSha256?input=password123'),
@@ -271,15 +215,9 @@ class SecurityCodecsSpec extends Specification {
response.status.code == 200
response.body().hashLength == 64
response.body().sha256Hash ==~ /^[a-f0-9]{64}$/
-
- cleanup:
- client.close()
}
def "test SHA256 bytes produces 32 bytes"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeSha256Bytes?input=password123'),
@@ -289,17 +227,11 @@ class SecurityCodecsSpec extends Specification {
then: "SHA256 bytes should be 32 bytes"
response.status.code == 200
response.body().bytesLength == 32
-
- cleanup:
- client.close()
}
// ========== Hex Encoding Tests ==========
def "test Hex encoding and decoding"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeHex?input=Hello'),
@@ -311,17 +243,11 @@ class SecurityCodecsSpec extends Specification {
response.body().input == 'Hello'
response.body().hexEncoded == '48656c6c6f' // "Hello" in hex
response.body().decodedBack == 'Hello'
-
- cleanup:
- client.close()
}
// ========== JavaScript Encoding Tests ==========
def "test JavaScript encoding escapes quotes and newlines"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET("/codecTest/encodeJavaScript"),
@@ -335,17 +261,11 @@ class SecurityCodecsSpec extends Specification {
response.body().input.contains('\n')
// The encoded output should escape these characters
response.body().encoded.contains("\\'") || response.body().encoded.contains("\\u0027")
-
- cleanup:
- client.close()
}
// ========== Raw Output Tests ==========
def "test Raw encoding preserves content without escaping"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeRaw'),
@@ -356,17 +276,11 @@ class SecurityCodecsSpec extends Specification {
response.status.code == 200
response.body().input == '<b>Bold</b>'
response.body().raw == '<b>Bold</b>'
-
- cleanup:
- client.close()
}
// ========== Multiple Encodings Tests ==========
def "test chaining multiple encodings"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/multipleEncodings'),
@@ -378,17 +292,11 @@ class SecurityCodecsSpec extends Specification {
response.body().input == '<script>alert(1)</script>'
response.body().htmlEncoded.contains('<')
response.body().fullyDecoded == '<script>alert(1)</script>'
-
- cleanup:
- client.close()
}
// ========== Special Characters Tests ==========
def "test encoding with Unicode and special characters"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeSpecialChars?input=%E6%97%A5%E6%9C%AC%E8%AA%9E+%26+%C3%A9moji+%F0%9F%91%8D+%3Ctag%3E'),
@@ -401,17 +309,11 @@ class SecurityCodecsSpec extends Specification {
response.body().htmlEncoded.contains('<tag>')
response.body().urlEncoded != null
response.body().base64Encoded != null
-
- cleanup:
- client.close()
}
// ========== Null Handling Tests ==========
def "test encoding null values returns null safely"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeNull'),
@@ -423,17 +325,11 @@ class SecurityCodecsSpec extends Specification {
response.body().nullBase64 == null
response.body().nullMd5 == null
response.body().nullHtml == null
-
- cleanup:
- client.close()
}
// ========== Empty String Tests ==========
def "test encoding empty strings"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeEmpty'),
@@ -450,17 +346,11 @@ class SecurityCodecsSpec extends Specification {
response.body().md5Hash.length() == 32
response.body().sha256Hash != null
response.body().sha256Hash.length() == 64
-
- cleanup:
- client.close()
}
// ========== Hash Consistency Tests ==========
def "test hash functions produce consistent results"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/hashConsistency?input=test-consistency'),
@@ -472,15 +362,9 @@ class SecurityCodecsSpec extends Specification {
response.body().md5Consistent == true
response.body().sha1Consistent == true
response.body().sha256Consistent == true
-
- cleanup:
- client.close()
}
def "test different inputs produce different hashes"() {
- given:
- def client = createClient()
-
when:
def response1 = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/hashConsistency?input=input1'),
@@ -497,17 +381,11 @@ class SecurityCodecsSpec extends Specification {
response1.body().md5Hash != response2.body().md5Hash
response1.body().sha1Hash != response2.body().sha1Hash
response1.body().sha256Hash != response2.body().sha256Hash
-
- cleanup:
- client.close()
}
// ========== Known Hash Values Tests ==========
def "test MD5 produces known hash for 'hello'"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeMd5?input=hello'),
@@ -517,15 +395,9 @@ class SecurityCodecsSpec extends Specification {
then: "MD5 of 'hello' should match known value"
response.status.code == 200
response.body().md5Hash == '5d41402abc4b2a76b9719d911017c592'
-
- cleanup:
- client.close()
}
def "test SHA1 produces known hash for 'hello'"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeSha1?input=hello'),
@@ -535,15 +407,9 @@ class SecurityCodecsSpec extends Specification {
then: "SHA1 of 'hello' should match known value"
response.status.code == 200
response.body().sha1Hash == 'aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d'
-
- cleanup:
- client.close()
}
def "test SHA256 produces known hash for 'hello'"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/codecTest/encodeSha256?input=hello'),
@@ -553,8 +419,5 @@ class SecurityCodecsSpec extends Specification {
then: "SHA256 of 'hello' should match known value"
response.status.code == 200
response.body().sha256Hash == '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
-
- cleanup:
- client.close()
}
}
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/commanddi/CommandObjectDISpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/commanddi/CommandObjectDISpec.groovy
index c8deb67716..0762550402 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/commanddi/CommandObjectDISpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/commanddi/CommandObjectDISpec.groovy
@@ -16,36 +16,40 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.commanddi
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
+import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
+import grails.testing.mixin.integration.Integration
+
/**
* Integration tests for Command Objects with Dependency Injection.
* Tests the ability to inject Spring services into Grails command objects.
*/
-@Integration(applicationClass = Application)
+@Integration
class CommandObjectDISpec extends Specification {
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
+ @Shared
+ HttpClient client
+
+ def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
+ }
+
+ def cleanupSpec() {
+ client.close()
}
// ========== Basic Service Injection Tests ==========
def "service is properly injected into command object"() {
- given: "a request to test service injection"
- def client = createClient()
-
when: "calling the test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/testServiceInjection"),
+ HttpRequest.GET('/commandDI/testServiceInjection'),
Map
)
@@ -53,18 +57,12 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().serviceInjected == true
response.body().serviceId == 'ValidationHelperService-v1'
-
- cleanup:
- client.close()
}
def "multiple services can be injected into single command object"() {
- given: "a request to test multiple service injection"
- def client = createClient()
-
when: "calling the endpoint with order command"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/testMultipleServices"),
+ HttpRequest.GET('/commandDI/testMultipleServices'),
Map
)
@@ -74,18 +72,12 @@ class CommandObjectDISpec extends Specification {
response.body().notificationServiceInjected == true
response.body().pricingServiceId == 'PricingService-v1'
response.body().notificationServiceId == 'NotificationService-v1'
-
- cleanup:
- client.close()
}
def "@Autowired annotation works for service injection"() {
- given: "a request to test @Autowired injection"
- def client = createClient()
-
when: "calling the endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/testAutowiredAnnotation"),
+ HttpRequest.GET('/commandDI/testAutowiredAnnotation'),
Map
)
@@ -93,20 +85,14 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().serviceInjected == true
response.body().serviceId == 'ValidationHelperService-v1'
-
- cleanup:
- client.close()
}
// ========== Validation with Injected Service Tests ==========
def "custom validation using injected service - valid username"() {
- given: "a valid user registration request"
- def client = createClient()
-
when: "registering with valid data"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/registerUser?username=johndoe&email=john@example.com&age=25"),
+ HttpRequest.GET('/commandDI/registerUser?username=johndoe&email=john@example.com&age=25'),
Map
)
@@ -116,18 +102,12 @@ class CommandObjectDISpec extends Specification {
response.body().errors.isEmpty()
response.body().username == 'johndoe'
response.body().usernameAvailable == true
-
- cleanup:
- client.close()
}
def "custom validation using injected service - reserved username rejected"() {
- given: "a registration request with reserved username"
- def client = createClient()
-
when: "registering with reserved username 'admin'"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/registerUser?username=admin&email=admin@example.com"),
+ HttpRequest.GET('/commandDI/registerUser?username=admin&email=admin@example.com'),
Map
)
@@ -136,16 +116,10 @@ class CommandObjectDISpec extends Specification {
response.body().valid == false
response.body().errors.any { it.field == 'username' }
response.body().usernameAvailable == false
-
- cleanup:
- client.close()
}
@Unroll
def "email domain validation - #email is #expectedResult"() {
- given: "a registration request with specific email"
- def client = createClient()
-
when: "registering with specific email"
def response = client.toBlocking().exchange(
HttpRequest.GET("/commandDI/registerUser?username=testuser&email=${URLEncoder.encode(email, 'UTF-8')}"),
@@ -156,9 +130,6 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().valid == expectedValid
- cleanup:
- client.close()
-
where:
email | expectedValid | expectedResult
'user@example.com' | true | 'valid'
@@ -170,9 +141,6 @@ class CommandObjectDISpec extends Specification {
@Unroll
def "age validation - age #age is #expectedResult"() {
- given: "a registration request with specific age"
- def client = createClient()
-
when: "registering with specific age"
def response = client.toBlocking().exchange(
HttpRequest.GET("/commandDI/registerUser?username=testuser&email=user@example.com&age=${age}"),
@@ -187,9 +155,6 @@ class CommandObjectDISpec extends Specification {
response.body().errors.any { it.field == 'age' }
}
- cleanup:
- client.close()
-
where:
age | expectedValid | expectedResult
18 | true | 'valid (minimum)'
@@ -200,48 +165,33 @@ class CommandObjectDISpec extends Specification {
}
def "phone number validation using service"() {
- given: "a registration request with phone number"
- def client = createClient()
-
when: "registering with valid phone"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/registerUser?username=testuser&email=user@example.com&phone=555-123-4567"),
+ HttpRequest.GET('/commandDI/registerUser?username=testuser&email=user@example.com&phone=555-123-4567'),
Map
)
then: "validation passes for valid phone"
response.status.code == 200
response.body().errors.findAll { it.field == 'phone' }.isEmpty()
-
- cleanup:
- client.close()
}
def "phone number validation fails for invalid phone"() {
- given: "a registration request with invalid phone"
- def client = createClient()
-
when: "registering with too short phone"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/registerUser?username=testuser&email=user@example.com&phone=123"),
+ HttpRequest.GET('/commandDI/registerUser?username=testuser&email=user@example.com&phone=123'),
Map
)
then: "validation fails for invalid phone"
response.status.code == 200
response.body().errors.any { it.field == 'phone' }
-
- cleanup:
- client.close()
}
// ========== Calculated Properties Tests ==========
@Unroll
def "discount calculation - quantity #quantity gives #expectedDiscount discount"() {
- given: "an order with specific quantity"
- def client = createClient()
-
when: "calculating order"
def response = client.toBlocking().exchange(
HttpRequest.GET("/commandDI/calculateOrder?productName=Widget&quantity=${quantity}&unitPrice=10.00"),
@@ -252,9 +202,6 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().discount == expectedDiscount
- cleanup:
- client.close()
-
where:
quantity | expectedDiscount
1 | 0.0
@@ -268,30 +215,21 @@ class CommandObjectDISpec extends Specification {
}
def "total price calculation with discount"() {
- given: "an order for 100 items at 10 dollars each"
- def client = createClient()
-
when: "calculating order"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/calculateOrder?productName=Widget&quantity=100&unitPrice=10.00"),
+ HttpRequest.GET('/commandDI/calculateOrder?productName=Widget&quantity=100&unitPrice=10.00'),
Map
)
then: "total price includes 25% discount (100*10=1000, minus 25% = 750)"
response.status.code == 200
response.body().totalPrice == 750.0
-
- cleanup:
- client.close()
}
def "price with tax calculation"() {
- given: "an order for 10 items at 100 dollars each"
- def client = createClient()
-
when: "calculating order"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/calculateOrder?productName=Premium+Widget&quantity=10&unitPrice=100.00"),
+ HttpRequest.GET('/commandDI/calculateOrder?productName=Premium+Widget&quantity=10&unitPrice=100.00'),
Map
)
@@ -299,20 +237,14 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().totalPrice == 900.0
response.body().priceWithTax == 972.0
-
- cleanup:
- client.close()
}
// ========== Order Validation Tests ==========
def "order validation with valid data"() {
- given: "a valid order"
- def client = createClient()
-
when: "validating order"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/validateOrder?productName=Widget&quantity=5&unitPrice=25.00"),
+ HttpRequest.GET('/commandDI/validateOrder?productName=Widget&quantity=5&unitPrice=25.00'),
Map
)
@@ -320,18 +252,12 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().valid == true
response.body().errors.isEmpty()
-
- cleanup:
- client.close()
}
def "order validation fails for price out of range"() {
- given: "an order with price too high"
- def client = createClient()
-
when: "validating order with excessive price"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/validateOrder?productName=Widget&quantity=5&unitPrice=99999.00"),
+ HttpRequest.GET('/commandDI/validateOrder?productName=Widget&quantity=5&unitPrice=99999.00'),
Map
)
@@ -339,20 +265,14 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().valid == false
response.body().errors.any { it.field == 'unitPrice' }
-
- cleanup:
- client.close()
}
// ========== Service Method Invocation Tests ==========
def "command object can invoke service method to send notification"() {
- given: "a notification request"
- def client = createClient()
-
when: "sending order notification"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/sendOrderNotification?customerEmail=customer@example.com&orderId=ORD-12345&message=Thank+you+for+your+order!"),
+ HttpRequest.GET('/commandDI/sendOrderNotification?customerEmail=customer@example.com&orderId=ORD-12345&message=Thank+you+for+your+order!'),
Map
)
@@ -361,20 +281,14 @@ class CommandObjectDISpec extends Specification {
response.body().notificationSent == true
response.body().lastNotification.recipient == 'customer@example.com'
response.body().lastNotification.message == 'Thank you for your order!'
-
- cleanup:
- client.close()
}
// ========== Prototype Scope Tests ==========
def "command object is prototype scoped - fresh instance per request"() {
- given: "two separate requests"
- def client = createClient()
-
when: "making first request"
def response1 = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/testPrototypeScope"),
+ HttpRequest.GET('/commandDI/testPrototypeScope'),
Map
)
@@ -384,7 +298,7 @@ class CommandObjectDISpec extends Specification {
when: "making second request"
def response2 = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/testPrototypeScope"),
+ HttpRequest.GET('/commandDI/testPrototypeScope'),
Map
)
@@ -392,20 +306,14 @@ class CommandObjectDISpec extends Specification {
response2.status.code == 200
response2.body().counter == 2
response2.body().expectedValue == 2
-
- cleanup:
- client.close()
}
// ========== Service Persistence Tests ==========
def "service remains injected after validation"() {
- given: "a request to test service persistence"
- def client = createClient()
-
when: "testing service after validation"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/testServiceAfterValidation?username=admin&email=test@example.com"),
+ HttpRequest.GET('/commandDI/testServiceAfterValidation?username=admin&email=test@example.com'),
Map
)
@@ -413,20 +321,14 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().serviceAfterFirst == true
response.body().serviceAfterSecond == true
-
- cleanup:
- client.close()
}
// ========== Optional Service Tests ==========
def "missing optional service is handled gracefully"() {
- given: "a command with optional service"
- def client = createClient()
-
when: "testing optional service"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/testOptionalService?data=test"),
+ HttpRequest.GET('/commandDI/testOptionalService?data=test'),
Map
)
@@ -434,20 +336,14 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().requiredServicePresent == true
response.body().optionalServicePresent == false
-
- cleanup:
- client.close()
}
// ========== State Dependent Validation Tests ==========
def "validation can depend on service state"() {
- given: "a request with price"
- def client = createClient()
-
when: "validating with positive price"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/validateWithServiceState?price=100.00"),
+ HttpRequest.GET('/commandDI/validateWithServiceState?price=100.00'),
Map
)
@@ -455,36 +351,24 @@ class CommandObjectDISpec extends Specification {
response.status.code == 200
response.body().valid == true
response.body().currentTaxRate == 0.08
-
- cleanup:
- client.close()
}
// ========== Edge Cases ==========
def "command object handles valid user request"() {
- given: "a basic valid request"
- def client = createClient()
-
when: "validating user"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/commandDI/registerUser?username=validuser&email=user@example.com"),
+ HttpRequest.GET('/commandDI/registerUser?username=validuser&email=user@example.com'),
Map
)
then: "request succeeds"
response.status.code == 200
response.body().username == 'validuser'
-
- cleanup:
- client.close()
}
@Unroll
def "reserved username '#username' is rejected"() {
- given: "a registration request with reserved username"
- def client = createClient()
-
when: "registering with reserved username"
def response = client.toBlocking().exchange(
HttpRequest.GET("/commandDI/registerUser?username=${username}&email=test@example.com"),
@@ -496,9 +380,6 @@ class CommandObjectDISpec extends Specification {
response.body().valid == false
response.body().usernameAvailable == false
- cleanup:
- client.close()
-
where:
username << ['admin', 'root', 'system', 'administrator', 'ADMIN', 'Root']
}
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/constraints/ConstraintValidationSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/constraints/ConstraintValidationSpec.groovy
index edc0675ac8..25f8487515 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/constraints/ConstraintValidationSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/constraints/ConstraintValidationSpec.groovy
@@ -16,21 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.constraints
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
import spock.lang.Specification
import spock.lang.Unroll
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
+
/**
* Integration tests for GORM constraint validation.
* Tests built-in constraints, custom validators, and cross-field validation.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class ConstraintValidationSpec extends Specification {
// ========== Product Constraints Tests ==========
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/contentneg/ContentNegotiationSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/contentneg/ContentNegotiationSpec.groovy
index 018eab6bdb..307561404e 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/contentneg/ContentNegotiationSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/contentneg/ContentNegotiationSpec.groovy
@@ -16,21 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.contentneg
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import groovy.json.JsonSlurper
+
import io.micronaut.http.HttpRequest
-import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import spock.lang.Narrative
+import spock.lang.Shared
import spock.lang.Specification
+import grails.testing.mixin.integration.Integration
+
/**
* Integration tests for Grails content negotiation features.
*
@@ -38,7 +38,7 @@ import spock.lang.Specification
* format selection, respond method, withFormat, and various
* content type handling scenarios.
*/
-@Integration(applicationClass = Application)
+@Integration
@Narrative('''
Grails provides content negotiation that allows the same controller action
to return different response formats (JSON, XML, HTML) based on the client's
@@ -46,18 +46,22 @@ Accept header or URL extension.
''')
class ContentNegotiationSpec extends Specification {
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
+ @Shared
+ HttpClient client
+
+ def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
+ }
+
+ def cleanupSpec() {
+ client.close()
}
// ========== Accept Header-Based Negotiation ==========
def "JSON response via Accept header application/json"() {
- given:
- def client = createClient()
-
when: "requesting with Accept: application/json"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/index')
.accept(MediaType.APPLICATION_JSON),
String
@@ -71,17 +75,11 @@ class ContentNegotiationSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.message == 'Hello World'
json.items.size() == 3
-
- cleanup:
- client.close()
}
def "XML response via Accept header application/xml"() {
- given:
- def client = createClient()
-
when: "requesting with Accept: application/xml"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/index')
.accept(MediaType.APPLICATION_XML),
String
@@ -93,17 +91,11 @@ class ContentNegotiationSpec extends Specification {
and: "content contains expected XML elements (Grails XML converter uses entry key format for maps)"
response.body().contains('<entry key="message">Hello World</entry>')
-
- cleanup:
- client.close()
}
def "HTML response via Accept header text/html"() {
- given:
- def client = createClient()
-
when: "requesting with Accept: text/html"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/index')
.accept(MediaType.TEXT_HTML),
String
@@ -116,19 +108,13 @@ class ContentNegotiationSpec extends Specification {
and: "content is HTML page"
response.body().contains('<h1>Content Negotiation</h1>')
response.body().contains('Hello World')
-
- cleanup:
- client.close()
}
// ========== URL Extension-Based Negotiation ==========
def "JSON response via .json extension"() {
- given:
- def client = createClient()
-
when: "requesting URL with .json extension"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/index.json'),
String
)
@@ -139,17 +125,11 @@ class ContentNegotiationSpec extends Specification {
and: "content is valid JSON with expected data"
def json = new JsonSlurper().parseText(response.body())
json.message == 'Hello World'
-
- cleanup:
- client.close()
}
def "XML response via .xml extension"() {
- given:
- def client = createClient()
-
when: "requesting URL with .xml extension"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/index.xml'),
String
)
@@ -159,19 +139,13 @@ class ContentNegotiationSpec extends Specification {
and: "content is XML (Grails converter uses map/entry format)"
response.body().contains('<entry key="message">')
-
- cleanup:
- client.close()
}
// ========== Respond Method Tests ==========
def "respond method returns JSON for Accept application/json"() {
- given:
- def client = createClient()
-
when: "calling respond action with Accept: application/json"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/respond')
.accept(MediaType.APPLICATION_JSON),
String
@@ -185,17 +159,11 @@ class ContentNegotiationSpec extends Specification {
json.status == 'success'
json.data.id == 1
json.data.name == 'Test Item'
-
- cleanup:
- client.close()
}
def "respond method returns XML for Accept application/xml"() {
- given:
- def client = createClient()
-
when: "calling respond action with Accept: application/xml"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/respond')
.accept(MediaType.APPLICATION_XML),
String
@@ -204,19 +172,13 @@ class ContentNegotiationSpec extends Specification {
then: "response is XML"
response.status == HttpStatus.OK
response.contentType.get().toString().contains('xml')
-
- cleanup:
- client.close()
}
// ========== List/Collection Content Negotiation ==========
def "list action returns JSON array"() {
- given:
- def client = createClient()
-
when: "requesting list with Accept: application/json"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/list')
.accept(MediaType.APPLICATION_JSON),
String
@@ -231,17 +193,11 @@ class ContentNegotiationSpec extends Specification {
json.size() == 3
json[0].id == 1
json[0].name == 'Item 1'
-
- cleanup:
- client.close()
}
def "list action returns XML for XML accept"() {
- given:
- def client = createClient()
-
when: "requesting list with Accept: application/xml"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/list')
.accept(MediaType.APPLICATION_XML),
String
@@ -250,19 +206,13 @@ class ContentNegotiationSpec extends Specification {
then: "response is XML"
response.status == HttpStatus.OK
response.contentType.get().toString().contains('xml')
-
- cleanup:
- client.close()
}
// ========== Explicit Content Type ==========
def "explicit content type overrides negotiation"() {
- given:
- def client = createClient()
-
when: "calling action with explicit content type"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/explicitContentType'),
String
)
@@ -274,17 +224,11 @@ class ContentNegotiationSpec extends Specification {
and: "content is as specified"
def json = new JsonSlurper().parseText(response.body())
json.explicit == true
-
- cleanup:
- client.close()
}
// ========== Error Response Formatting ==========
def "error response in JSON format"() {
- given:
- def client = createClient()
-
when: "requesting error action with Accept: application/json"
client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/error')
@@ -293,7 +237,7 @@ class ContentNegotiationSpec extends Specification {
)
then: "error response is returned"
- HttpClientResponseException e = thrown()
+ def e = thrown(HttpClientResponseException)
e.status == HttpStatus.BAD_REQUEST
and: "error body is JSON"
@@ -301,15 +245,9 @@ class ContentNegotiationSpec extends Specification {
json.error == true
json.message == 'Something went wrong'
json.code == 'ERR_001'
-
- cleanup:
- client.close()
}
def "error response in XML format"() {
- given:
- def client = createClient()
-
when: "requesting error action with Accept: application/xml"
client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/error')
@@ -318,24 +256,18 @@ class ContentNegotiationSpec extends Specification {
)
then: "error response is returned"
- HttpClientResponseException e = thrown()
+ def e = thrown(HttpClientResponseException)
e.status == HttpStatus.BAD_REQUEST
and: "error body contains XML elements (Grails converter uses entry key format)"
e.response.body().toString().contains('<entry key="error">true</entry>')
-
- cleanup:
- client.close()
}
// ========== Format Parameter Override ==========
def "format parameter can specify JSON"() {
- given:
- def client = createClient()
-
when: "requesting with format=json parameter"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/formatParam?format=json'),
String
)
@@ -347,17 +279,11 @@ class ContentNegotiationSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.format == 'json'
json.value == 42
-
- cleanup:
- client.close()
}
def "format parameter can specify XML"() {
- given:
- def client = createClient()
-
when: "requesting with format=xml parameter"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/formatParam?format=xml'),
String
)
@@ -365,19 +291,13 @@ class ContentNegotiationSpec extends Specification {
then: "response is XML"
response.status == HttpStatus.OK
response.body().contains('<entry key="format">xml</entry>')
-
- cleanup:
- client.close()
}
// ========== Status Code Tests ==========
def "status by format returns correct status for JSON"() {
- given:
- def client = createClient()
-
when: "requesting statusByFormat with Accept: application/json"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/statusByFormat')
.accept(MediaType.APPLICATION_JSON),
String
@@ -389,19 +309,13 @@ class ContentNegotiationSpec extends Specification {
and: "body is JSON"
def json = new JsonSlurper().parseText(response.body())
json.status == 'ok'
-
- cleanup:
- client.close()
}
// ========== Accept Header Quality Values ==========
def "multiAccept action handles Accept header"() {
- given:
- def client = createClient()
-
when: "requesting with complex Accept header"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/multiAccept')
.accept(MediaType.APPLICATION_JSON),
String
@@ -413,19 +327,13 @@ class ContentNegotiationSpec extends Specification {
and: "response contains accept header info"
def json = new JsonSlurper().parseText(response.body())
json.acceptHeader != null
-
- cleanup:
- client.close()
}
// ========== Wildcard/Default Format ==========
def "formatParam falls back to default for unknown format"() {
- given:
- def client = createClient()
-
when: "requesting with unknown format"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/formatParam?format=unknown'),
String
)
@@ -437,19 +345,13 @@ class ContentNegotiationSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.format == 'unknown'
json.value == 42
-
- cleanup:
- client.close()
}
// ========== Content-Type Header Variations ==========
def "JSON with charset in Accept header"() {
- given:
- def client = createClient()
-
when: "requesting with Accept including charset"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/respond')
.header('Accept', 'application/json; charset=utf-8'),
String
@@ -458,19 +360,13 @@ class ContentNegotiationSpec extends Specification {
then: "response is JSON"
response.status == HttpStatus.OK
response.contentType.get().toString().contains('json')
-
- cleanup:
- client.close()
}
// ========== Multiple Format Extensions ==========
def "respond action with .json extension"() {
- given:
- def client = createClient()
-
when: "requesting respond with .json extension"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/respond.json'),
String
)
@@ -481,17 +377,11 @@ class ContentNegotiationSpec extends Specification {
and: "content is valid"
def json = new JsonSlurper().parseText(response.body())
json.status == 'success'
-
- cleanup:
- client.close()
}
def "list action with .json extension"() {
- given:
- def client = createClient()
-
when: "requesting list with .json extension"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/list.json'),
String
)
@@ -503,19 +393,13 @@ class ContentNegotiationSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json instanceof List
json.size() == 3
-
- cleanup:
- client.close()
}
// ========== Custom JSON Rendering ==========
def "custom JSON rendering produces valid output"() {
- given:
- def client = createClient()
-
when: "requesting custom JSON action"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/contentNegotiation/customJson'),
String
)
@@ -523,8 +407,5 @@ class ContentNegotiationSpec extends Specification {
then: "response is JSON"
response.status == HttpStatus.OK
response.contentType.get().toString().contains('application/json')
-
- cleanup:
- client.close()
}
}
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/cors/CorsAdvancedSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/cors/CorsAdvancedSpec.groovy
index bf86e37560..951db909f2 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/cors/CorsAdvancedSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/cors/CorsAdvancedSpec.groovy
@@ -18,17 +18,17 @@
*/
package functionaltests.cors
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
import groovy.json.JsonSlurper
+
import io.micronaut.http.HttpRequest
-import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
-import spock.lang.Specification
import spock.lang.Shared
+import spock.lang.Specification
+
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for CORS (Cross-Origin Resource Sharing) functionality.
@@ -36,26 +36,26 @@ import spock.lang.Shared
*
* Note: CORS is enabled for /api/** in application.yml
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class CorsAdvancedSpec extends Specification {
@Shared
HttpClient client
def setup() {
- client = HttpClient.create(new URL("http://localhost:${serverPort}"))
+ client = client ?: HttpClient.create(new URL("http://localhost:${serverPort}"))
}
- def cleanup() {
- client?.close()
+ def cleanupSpec() {
+ client.close()
}
// ========== Basic CORS Header Tests ==========
def "GET request to CORS-enabled endpoint includes CORS headers"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors')
.header('Origin', 'http://example.com'),
String
@@ -69,7 +69,7 @@ class CorsAdvancedSpec extends Specification {
def "OPTIONS preflight request returns appropriate headers"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.OPTIONS('/api/cors/data')
.header('Origin', 'http://example.com')
.header('Access-Control-Request-Method', 'GET'),
@@ -84,7 +84,7 @@ class CorsAdvancedSpec extends Specification {
def "preflight request for POST method succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.OPTIONS('/api/cors/items')
.header('Origin', 'http://example.com')
.header('Access-Control-Request-Method', 'POST')
@@ -100,7 +100,7 @@ class CorsAdvancedSpec extends Specification {
def "preflight request for PUT method succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.OPTIONS('/api/cors/items/1')
.header('Origin', 'http://example.com')
.header('Access-Control-Request-Method', 'PUT')
@@ -114,7 +114,7 @@ class CorsAdvancedSpec extends Specification {
def "preflight request for DELETE method succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.OPTIONS('/api/cors/items/1')
.header('Origin', 'http://example.com')
.header('Access-Control-Request-Method', 'DELETE'),
@@ -129,7 +129,7 @@ class CorsAdvancedSpec extends Specification {
def "GET request to CORS endpoint returns data"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors/data')
.header('Origin', 'http://example.com'),
String
@@ -144,7 +144,7 @@ class CorsAdvancedSpec extends Specification {
def "POST request with CORS headers succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/api/cors/items', '{"name":"New Item"}')
.header('Origin', 'http://example.com')
.contentType(MediaType.APPLICATION_JSON),
@@ -160,7 +160,7 @@ class CorsAdvancedSpec extends Specification {
def "PUT request with CORS headers succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.PUT('/api/cors/items/42', '{"name":"Updated Item"}')
.header('Origin', 'http://example.com')
.contentType(MediaType.APPLICATION_JSON),
@@ -177,7 +177,7 @@ class CorsAdvancedSpec extends Specification {
def "DELETE request with CORS headers succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.DELETE('/api/cors/items/99')
.header('Origin', 'http://example.com'),
String
@@ -195,7 +195,7 @@ class CorsAdvancedSpec extends Specification {
def "response with custom headers includes CORS headers"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors/custom-headers')
.header('Origin', 'http://example.com'),
String
@@ -210,7 +210,7 @@ class CorsAdvancedSpec extends Specification {
def "echo origin endpoint returns received origin"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors/echo-origin')
.header('Origin', 'http://my-app.example.com'),
String
@@ -226,7 +226,7 @@ class CorsAdvancedSpec extends Specification {
def "authenticated endpoint receives authorization header"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors/authenticated')
.header('Origin', 'http://example.com')
.header('Authorization', 'Bearer test-token-123'),
@@ -242,7 +242,7 @@ class CorsAdvancedSpec extends Specification {
def "authenticated endpoint without credentials returns unauthenticated"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors/authenticated')
.header('Origin', 'http://example.com'),
String
@@ -259,7 +259,7 @@ class CorsAdvancedSpec extends Specification {
def "request from localhost origin succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors')
.header('Origin', 'http://localhost:3000'),
String
@@ -273,7 +273,7 @@ class CorsAdvancedSpec extends Specification {
def "request from HTTPS origin succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors')
.header('Origin', 'https://secure.example.com'),
String
@@ -287,7 +287,7 @@ class CorsAdvancedSpec extends Specification {
def "request with port in origin succeeds"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/cors')
.header('Origin', 'http://example.com:8080'),
String
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/errorhandling/ErrorHandlingSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/errorhandling/ErrorHandlingSpec.groovy
index 9c28026cbc..79ff98bea7 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/errorhandling/ErrorHandlingSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/errorhandling/ErrorHandlingSpec.groovy
@@ -18,265 +18,119 @@
*/
package functionaltests.errorhandling
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
import groovy.json.JsonSlurper
+
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
-import spock.lang.Specification
import spock.lang.Shared
+import spock.lang.Specification
import spock.lang.Unroll
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
+
/**
* Integration tests for error handling patterns in Grails controllers.
* Tests various HTTP status codes, JSON error responses, exception handling,
* and error response headers.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class ErrorHandlingSpec extends Specification {
@Shared
HttpClient client
def setup() {
- client = HttpClient.create(new URL("http://localhost:${serverPort}"))
+ client = client ?: HttpClient.create(new URL("http://localhost:${serverPort}"))
}
- def cleanup() {
- client?.close()
+ def cleanupSpec() {
+ client.close()
}
// ========== HTTP Status Code Tests ==========
- def "render 404 Not Found status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderNotFound'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.NOT_FOUND
- }
-
- def "render 400 Bad Request status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderBadRequest'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.BAD_REQUEST
- }
-
- def "render 401 Unauthorized status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderUnauthorized'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.UNAUTHORIZED
- }
-
- def "render 403 Forbidden status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderForbidden'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.FORBIDDEN
- }
-
- def "render 405 Method Not Allowed status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderMethodNotAllowed'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.METHOD_NOT_ALLOWED
- }
-
- def "render 409 Conflict status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderConflict'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.CONFLICT
- }
-
- def "render 410 Gone status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderGone'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.GONE
- }
-
- def "render 422 Unprocessable Entity status"() {
+ @Unroll
+ def "render #statusMsg status"(String action, HttpStatus status, String statusMsg) {
when:
client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderUnprocessableEntity'),
+ HttpRequest.GET("/errorHandlingTest/$action"),
String
)
then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.UNPROCESSABLE_ENTITY
- }
+ def e = thrown(HttpClientResponseException)
+ e.status == status
- def "render 429 Too Many Requests status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderTooManyRequests'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.TOO_MANY_REQUESTS
- }
-
- def "render 500 Internal Server Error status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderInternalServerError'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.INTERNAL_SERVER_ERROR
- }
-
- def "render 503 Service Unavailable status"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/renderServiceUnavailable'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.SERVICE_UNAVAILABLE
+ where:
+ action | status | statusMsg
+ 'renderNotFound' | HttpStatus.NOT_FOUND | '404 Not Found'
+ 'renderBadRequest' | HttpStatus.BAD_REQUEST | '400 Bad Request'
+ 'renderUnauthorized' | HttpStatus.UNAUTHORIZED | '401 Unauthorized'
+ 'renderForbidden' | HttpStatus.FORBIDDEN | '403 Forbidden'
+ 'renderMethodNotAllowed' | HttpStatus.METHOD_NOT_ALLOWED | '405 Method Not Allowed'
+ 'renderConflict' | HttpStatus.CONFLICT | '409 Conflict'
+ 'renderGone' | HttpStatus.GONE | '410 Gone'
+ 'renderUnprocessableEntity' | HttpStatus.UNPROCESSABLE_ENTITY | '422 Unprocessable Entity'
+ 'renderTooManyRequests' | HttpStatus.TOO_MANY_REQUESTS | '429 Too Many Requests'
+ 'renderInternalServerError' | HttpStatus.INTERNAL_SERVER_ERROR | '500 Internal Server Error'
+ 'renderServiceUnavailable' | HttpStatus.SERVICE_UNAVAILABLE | '503 Service Unavailable'
}
// ========== JSON Error Response Tests ==========
- def "JSON 404 error response contains proper structure"() {
+ @Unroll
+ def "JSON #statusCode.code error response #assertion"(String action, HttpStatus statusCode, String assertion) {
when:
client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/jsonNotFound').accept('application/json'),
+ HttpRequest.GET("/errorHandlingTest/$action")
+ .accept('application/json'),
String
)
then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.NOT_FOUND
- }
-
- def "JSON 400 error response with validation details"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/jsonBadRequest').accept('application/json'),
- String
- )
+ def e = thrown(HttpClientResponseException)
+ e.status == statusCode
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.BAD_REQUEST
- }
-
- def "JSON 422 validation error with multiple field errors"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/jsonValidationError').accept('application/json'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.UNPROCESSABLE_ENTITY
- }
-
- def "JSON 500 error response includes request ID"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/jsonServerError').accept('application/json'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.INTERNAL_SERVER_ERROR
+ where:
+ action | statusCode | assertion
+ 'jsonNotFound' | HttpStatus.NOT_FOUND | 'contains proper structure'
+ 'jsonBadRequest' | HttpStatus.BAD_REQUEST | 'with validation details'
+ 'jsonValidationError' | HttpStatus.UNPROCESSABLE_ENTITY | 'with multiple field errors'
+ 'jsonServerError' | HttpStatus.INTERNAL_SERVER_ERROR | 'includes request ID'
}
// ========== Conditional Error Handling Tests ==========
- def "conditional error returns 404 when condition is notfound"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/conditionalError?condition=notfound').accept('application/json'),
- String
- )
-
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.NOT_FOUND
- }
-
- def "conditional error returns 400 when condition is badrequest"() {
+ @Unroll
+ def "conditional error returns #status.code when condition is #condition"(String condition, HttpStatus status) {
when:
client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/conditionalError?condition=badrequest').accept('application/json'),
+ HttpRequest.GET("/errorHandlingTest/conditionalError?condition=$condition")
+ .accept('application/json'),
String
)
then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.BAD_REQUEST
- }
-
- def "conditional error returns 403 when condition is forbidden"() {
- when:
- client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/conditionalError?condition=forbidden').accept('application/json'),
- String
- )
+ def e = thrown(HttpClientResponseException)
+ e.status == status
- then:
- HttpClientResponseException e = thrown()
- e.status == HttpStatus.FORBIDDEN
+ where:
+ condition | status
+ 'notfound' | HttpStatus.NOT_FOUND
+ 'badrequest' | HttpStatus.BAD_REQUEST
+ 'forbidden' | HttpStatus.FORBIDDEN
}
def "conditional error returns success for unknown condition"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
- HttpRequest.GET('/errorHandlingTest/conditionalError?condition=normal').accept('application/json'),
+ def response = client.toBlocking().exchange(
+ HttpRequest.GET('/errorHandlingTest/conditionalError?condition=normal')
+ .accept('application/json'),
String
)
@@ -299,7 +153,7 @@ class ErrorHandlingSpec extends Specification {
)
then:
- HttpClientResponseException e = thrown()
+ def e = thrown(HttpClientResponseException)
e.status == HttpStatus.TOO_MANY_REQUESTS
and:
@@ -318,7 +172,7 @@ class ErrorHandlingSpec extends Specification {
)
then:
- HttpClientResponseException e = thrown()
+ def e = thrown(HttpClientResponseException)
e.status == HttpStatus.NOT_FOUND
and: "suggestion header is present"
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/events/DomainEventsSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/events/DomainEventsSpec.groovy
index d553dbf8e7..8bae3cfb27 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/events/DomainEventsSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/events/DomainEventsSpec.groovy
@@ -19,11 +19,11 @@
package functionaltests.events
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
-import spock.lang.Specification
import spock.lang.Narrative
+import spock.lang.Specification
+
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for GORM domain lifecycle events.
@@ -32,8 +32,8 @@ import spock.lang.Narrative
* beforeDelete, afterDelete, beforeValidate, afterLoad events,
* auto-timestamping, dirty checking, and event veto capabilities.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
@Narrative('''
GORM provides lifecycle event hooks that are triggered during domain object
persistence operations. These events allow for automatic auditing, validation,
@@ -91,7 +91,7 @@ class DomainEventsSpec extends Specification {
def insertIndex = entity.eventLog.indexOf('beforeInsert')
def afterIndex = entity.eventLog.indexOf('afterInsert')
insertIndex >= 0
- afterIndex >= 0
+ afterIndex > 0
insertIndex < afterIndex
}
@@ -136,8 +136,8 @@ class DomainEventsSpec extends Specification {
then: "modified audit fields are updated"
entity.modifiedAt != null
entity.modifiedBy == 'system'
- // modifiedAt should be different from original (which was null)
- entity.modifiedAt != originalModifiedAt
+ // modifiedAt should be larger than original
+ entity.modifiedAt > originalModifiedAt
}
def "update events do not fire when entity is unchanged"() {
@@ -191,7 +191,7 @@ class DomainEventsSpec extends Specification {
def beforeIndex = entity.eventLog.indexOf('beforeDelete')
def afterIndex = entity.eventLog.indexOf('afterDelete')
beforeIndex >= 0
- afterIndex >= 0
+ afterIndex > 0
beforeIndex < afterIndex
}
@@ -230,6 +230,7 @@ class DomainEventsSpec extends Specification {
then: "beforeValidate was triggered"
entity.eventLog.contains('beforeValidate')
+
and: "name was trimmed"
entity.name == 'Trim On Save'
}
@@ -288,7 +289,7 @@ class DomainEventsSpec extends Specification {
entity.save(flush: true)
then: "lastUpdated is updated"
- entity.lastUpdated >= originalLastUpdated
+ entity.lastUpdated > originalLastUpdated
}
def "dateCreated remains unchanged on update"() {
@@ -296,7 +297,8 @@ class DomainEventsSpec extends Specification {
def entity = new AuditedEntity(name: 'DateCreated Test').save(flush: true)
def originalDateCreated = entity.dateCreated
- when: "the entity is updated"
+ when: "the entity is updated after a brief pause"
+ sleep(10)
entity.description = 'Modified'
entity.save(flush: true)
@@ -550,10 +552,8 @@ class DomainEventsSpec extends Specification {
def afterInsertIndex = entity.eventLog.indexOf('afterInsert')
validateIndex >= 0
- insertIndex >= 0
- afterInsertIndex >= 0
- // beforeValidate should come before beforeInsert
- validateIndex < insertIndex || insertIndex < afterInsertIndex
+ insertIndex > validateIndex
+ afterInsertIndex > insertIndex
}
def "multiple entities have independent event logs"() {
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy
index a81b33bbd5..a882e31214 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy
@@ -18,38 +18,38 @@
*/
package functionaltests.fileupload
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
import groovy.json.JsonSlurper
+
import io.micronaut.http.HttpRequest
-import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.http.client.multipart.MultipartBody
-import spock.lang.Specification
import spock.lang.Shared
+import spock.lang.Specification
+
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for file upload functionality in Grails.
* Tests various file upload patterns including single file, multiple files,
* file validation, and metadata extraction.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class FileUploadSpec extends Specification {
@Shared
HttpClient client
def setup() {
- client = HttpClient.create(new URL("http://localhost:${serverPort}"))
+ client = client ?: HttpClient.create(new URL("http://localhost:${serverPort}"))
}
- def cleanup() {
- client?.close()
+ def cleanupSpec() {
+ client.close()
}
// ========== Single File Upload Tests ==========
@@ -62,7 +62,7 @@ class FileUploadSpec extends Specification {
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadSingle', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -104,7 +104,7 @@ class FileUploadSpec extends Specification {
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadWithMetadata', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -129,7 +129,7 @@ class FileUploadSpec extends Specification {
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadMultiple', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -157,7 +157,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadTextFile', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -180,7 +180,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadAndEcho', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -202,7 +202,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadWithValidation', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -222,7 +222,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadWithExtensionValidation', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -244,7 +244,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadWithExtensionValidation', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -266,7 +266,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/getFileInfo', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -289,7 +289,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/getFileInfo', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -312,7 +312,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadViaParams', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -336,7 +336,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadSingle', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -357,7 +357,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadAndEcho', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
@@ -379,7 +379,7 @@ This is a longer line with more words'''
.build()
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/fileUploadTest/uploadAndEcho', body)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
String
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/gorm/GormAdvancedSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/gorm/GormAdvancedSpec.groovy
index 2a70fd3930..2a20fe93b9 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/gorm/GormAdvancedSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/gorm/GormAdvancedSpec.groovy
@@ -16,14 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.gorm
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
import spock.lang.Specification
-import spock.lang.Shared
+
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for advanced GORM features:
@@ -34,8 +32,8 @@ import spock.lang.Shared
* - Named queries
* - Detached criteria
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class GormAdvancedSpec extends Specification {
def setup() {
@@ -43,7 +41,12 @@ class GormAdvancedSpec extends Specification {
createTestData()
}
- private void createTestData() {
+ private static void createTestData() {
+
+ if (Author.count() > 0) {
+ return // Test data already exists
+ }
+
// Author 1: American author with 3 books
def author1 = new Author(name: 'John Smith', country: 'USA', birthYear: 1970, active: true)
author1.addToBooks(new GormBook(title: 'The Great Adventure', genre: 'Fiction', price: 19.99, pageCount: 350, publicationYear: 2020, rating: 4.5))
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/i18n/InternationalizationSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/i18n/InternationalizationSpec.groovy
index 0570ede1aa..1c7a33e142 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/i18n/InternationalizationSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/i18n/InternationalizationSpec.groovy
@@ -16,15 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.i18n
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
+import spock.lang.Shared
import spock.lang.Specification
+import grails.testing.mixin.integration.Integration
+
/**
* Comprehensive integration tests for internationalization (i18n) features.
*
@@ -38,19 +38,23 @@ import spock.lang.Specification
* - Validation error messages
* - Accept-Language header handling
*/
-@Integration(applicationClass = Application)
+@Integration
class InternationalizationSpec extends Specification {
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
+ @Shared
+ HttpClient client
+
+ def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
+ }
+
+ def cleanupSpec() {
+ client.close()
}
// ========== Basic Message Resolution Tests ==========
def "test simple message in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessage?code=app.welcome&lang=en'),
@@ -62,15 +66,9 @@ class InternationalizationSpec extends Specification {
response.body().code == 'app.welcome'
response.body().locale == 'en'
response.body().message == 'Welcome to the Application'
-
- cleanup:
- client.close()
}
def "test simple message in German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessage?code=app.welcome&lang=de'),
@@ -82,15 +80,9 @@ class InternationalizationSpec extends Specification {
response.body().code == 'app.welcome'
response.body().locale == 'de'
response.body().message == 'Willkommen in der Anwendung'
-
- cleanup:
- client.close()
}
def "test simple message in French"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessage?code=app.welcome&lang=fr'),
@@ -102,17 +94,11 @@ class InternationalizationSpec extends Specification {
response.body().code == 'app.welcome'
response.body().locale == 'fr'
response.body().message == "Bienvenue dans l'application"
-
- cleanup:
- client.close()
}
// ========== Message With Arguments Tests ==========
def "test message with argument in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessageWithArgs?code=app.greeting&arg=John&lang=en'),
@@ -122,15 +108,9 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().message == 'Hello, John!'
-
- cleanup:
- client.close()
}
def "test message with argument in German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessageWithArgs?code=app.greeting&arg=Johann&lang=de'),
@@ -140,15 +120,9 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().message == 'Hallo, Johann!'
-
- cleanup:
- client.close()
}
def "test farewell message with argument"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessageWithArgs?code=app.farewell&arg=Alice&lang=en'),
@@ -158,17 +132,11 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().message == 'Goodbye, Alice. See you soon!'
-
- cleanup:
- client.close()
}
// ========== Pluralization Tests (Choice Format) ==========
def "test choice format - zero items in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getChoiceMessage?count=0&lang=en'),
@@ -179,15 +147,9 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().count == 0
response.body().message == 'You have no items.'
-
- cleanup:
- client.close()
}
def "test choice format - one item in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getChoiceMessage?count=1&lang=en'),
@@ -198,15 +160,9 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().count == 1
response.body().message == 'You have one item.'
-
- cleanup:
- client.close()
}
def "test choice format - multiple items in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getChoiceMessage?count=5&lang=en'),
@@ -217,15 +173,9 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().count == 5
response.body().message == 'You have 5 items.'
-
- cleanup:
- client.close()
}
def "test choice format in German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getChoiceMessage?count=0&lang=de'),
@@ -235,15 +185,9 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().message == 'Sie haben keine Artikel.'
-
- cleanup:
- client.close()
}
def "test choice format - one item in German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getChoiceMessage?count=1&lang=de'),
@@ -253,17 +197,11 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().message == 'Sie haben einen Artikel.'
-
- cleanup:
- client.close()
}
// ========== Date Formatting Tests ==========
def "test date formatting in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getDateMessage?lang=en'),
@@ -275,15 +213,9 @@ class InternationalizationSpec extends Specification {
response.body().message.startsWith('Today is ')
// English format: "Today is January 25, 2026."
response.body().message.contains(',')
-
- cleanup:
- client.close()
}
def "test date formatting in German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getDateMessage?lang=de'),
@@ -294,17 +226,11 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().message.startsWith('Heute ist der ')
// German format: "Heute ist der 25. Januar 2026."
-
- cleanup:
- client.close()
}
// ========== Currency Formatting Tests ==========
def "test currency formatting in English US"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getCurrencyMessage?amount=1234.56&lang=en_US'),
@@ -314,15 +240,9 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().message.contains('$') || response.body().message.contains('1,234.56')
-
- cleanup:
- client.close()
}
def "test currency formatting in German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getCurrencyMessage?amount=1234.56&lang=de_DE'),
@@ -333,17 +253,11 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
// German format uses € and different number formatting
response.body().message != null
-
- cleanup:
- client.close()
}
// ========== Percentage Formatting Tests ==========
def "test percentage formatting in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getPercentMessage?value=0.75&lang=en'),
@@ -354,17 +268,11 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().message.contains('75')
response.body().message.contains('%')
-
- cleanup:
- client.close()
}
// ========== Default Message Fallback Tests ==========
def "test default message for non-existent code"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessageWithDefault?code=non.existent.key&defaultMsg=Fallback+Message&lang=en'),
@@ -374,17 +282,11 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().message == 'Fallback Message'
-
- cleanup:
- client.close()
}
// ========== Validation Messages Tests ==========
def "test validation messages in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getValidationMessages?lang=en'),
@@ -397,15 +299,9 @@ class InternationalizationSpec extends Specification {
response.body().messages.nullable.contains('cannot be null')
response.body().messages.paginate_prev == 'Previous'
response.body().messages.paginate_next == 'Next'
-
- cleanup:
- client.close()
}
def "test validation messages in German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getValidationMessages?lang=de'),
@@ -416,15 +312,9 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().messages.paginate_prev == 'Vorherige'
response.body().messages.paginate_next == 'Nächste'
-
- cleanup:
- client.close()
}
def "test validation messages in French"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getValidationMessages?lang=fr'),
@@ -435,17 +325,11 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().messages.paginate_prev == 'Précédent'
response.body().messages.paginate_next == 'Suivant'
-
- cleanup:
- client.close()
}
// ========== Multiple Messages Tests ==========
def "test multiple messages at once in English"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMultipleMessages?lang=en'),
@@ -457,15 +341,9 @@ class InternationalizationSpec extends Specification {
response.body().messages.welcome == 'Welcome to the Application'
response.body().messages.greeting == 'Hello, User!'
response.body().messages.farewell == 'Goodbye, User. See you soon!'
-
- cleanup:
- client.close()
}
def "test multiple messages at once in German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMultipleMessages?lang=de'),
@@ -477,17 +355,11 @@ class InternationalizationSpec extends Specification {
response.body().messages.welcome == 'Willkommen in der Anwendung'
response.body().messages.greeting == 'Hallo, User!'
response.body().messages.farewell == 'Auf Wiedersehen, User. Bis bald!'
-
- cleanup:
- client.close()
}
// ========== Locale Information Tests ==========
def "test current locale information"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getCurrentLocale?lang=de_DE'),
@@ -498,17 +370,11 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().language == 'de'
response.body().country == 'DE'
-
- cleanup:
- client.close()
}
// ========== Accept-Language Header Tests ==========
def "test locale from Accept-Language header - German"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getLocaleFromHeader')
@@ -520,15 +386,9 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
// The request locale should reflect the Accept-Language header
response.body().requestLocale?.startsWith('de') || response.body().contextLocale?.startsWith('de')
-
- cleanup:
- client.close()
}
def "test locale from Accept-Language header - French"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getLocaleFromHeader')
@@ -539,17 +399,11 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().requestLocale?.startsWith('fr') || response.body().contextLocale?.startsWith('fr')
-
- cleanup:
- client.close()
}
// ========== Controller Message Method Tests ==========
def "test controller message method"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/useControllerMessage?code=app.welcome&lang=en'),
@@ -559,17 +413,11 @@ class InternationalizationSpec extends Specification {
then:
response.status.code == 200
response.body().message == 'Welcome to the Application'
-
- cleanup:
- client.close()
}
// ========== Edge Cases ==========
def "test fallback to default locale when unsupported locale requested"() {
- given:
- def client = createClient()
-
when: "requesting a locale that doesn't have translations"
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessage?code=app.welcome&lang=xyz'),
@@ -580,15 +428,9 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
// Will either get the message or fall back
response.body().message != null
-
- cleanup:
- client.close()
}
def "test message with special characters"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/i18nTest/getMessageWithArgs?code=app.greeting&arg=%C3%A9l%C3%A8ve&lang=en'),
@@ -599,8 +441,5 @@ class InternationalizationSpec extends Specification {
response.status.code == 200
response.body().arg == 'élève'
response.body().message == 'Hello, élève!'
-
- cleanup:
- client.close()
}
}
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/interceptors/InterceptorOrderingSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/interceptors/InterceptorOrderingSpec.groovy
index da64200df5..acc1fcbfa4 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/interceptors/InterceptorOrderingSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/interceptors/InterceptorOrderingSpec.groovy
@@ -16,15 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.interceptors
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
+import spock.lang.Shared
import spock.lang.Specification
+import grails.testing.mixin.integration.Integration
+
/**
* Comprehensive integration tests for Grails interceptor functionality.
*
@@ -37,32 +37,28 @@ import spock.lang.Specification
* - Conditional matching
* - Timing/performance tracking
*/
-@Integration(applicationClass = Application)
+@Integration
class InterceptorOrderingSpec extends Specification {
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
- }
+ @Shared
+ HttpClient client
def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
// Reset execution order before each test
- def client = createClient()
- try {
- client.toBlocking().exchange(
+ client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/resetOrder'),
Map
- )
- } finally {
- client.close()
- }
+ )
+ }
+
+ def cleanupSpec() {
+ client.close()
}
// ========== Interceptor Ordering Tests ==========
def "test interceptors execute in order by 'order' property"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/testOrder'),
@@ -88,15 +84,9 @@ class InterceptorOrderingSpec extends Specification {
firstBeforeIdx < secondBeforeIdx
secondBeforeIdx < thirdBeforeIdx
thirdBeforeIdx < controllerIdx
-
- cleanup:
- client.close()
}
def "test before interceptors run before controller action"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/index'),
@@ -111,15 +101,9 @@ class InterceptorOrderingSpec extends Specification {
order.findAll { it.contains(':before') }.every { beforeEntry ->
order.indexOf(beforeEntry) < order.indexOf('controller:index')
}
-
- cleanup:
- client.close()
}
def "test after interceptors run after controller action"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/index'),
@@ -134,17 +118,11 @@ class InterceptorOrderingSpec extends Specification {
order.findAll { it.contains(':after') }.every { afterEntry ->
order.indexOf(afterEntry) > order.indexOf('controller:index')
}
-
- cleanup:
- client.close()
}
// ========== Blocking Interceptor Tests ==========
def "test interceptor can block request by returning false"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/blocked?block=true&reason=testing'),
@@ -156,15 +134,9 @@ class InterceptorOrderingSpec extends Specification {
response.body().blocked == true
response.body().message == 'Request blocked by interceptor'
response.body().reason == 'testing'
-
- cleanup:
- client.close()
}
def "test interceptor allows request when returning true"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/blocked?block=false'),
@@ -175,17 +147,11 @@ class InterceptorOrderingSpec extends Specification {
response.status.code == 200
response.body().action == 'blocked'
response.body().message == 'This should not be seen if blocked'
-
- cleanup:
- client.close()
}
// ========== Request Attribute Tests ==========
def "test interceptor can set request attributes"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/checkAttributes'),
@@ -197,15 +163,9 @@ class InterceptorOrderingSpec extends Specification {
response.body().fromBefore == true
response.body().interceptorSet != null
response.body().interceptorSet.source == 'AttributeSettingInterceptor'
-
- cleanup:
- client.close()
}
def "test interceptor can set response headers"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/checkAttributes'),
@@ -215,17 +175,11 @@ class InterceptorOrderingSpec extends Specification {
then:
response.status.code == 200
response.header('X-Interceptor-Header') == 'set-by-interceptor'
-
- cleanup:
- client.close()
}
// ========== Session Tests ==========
def "test interceptor can set session attributes"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/checkSession'),
@@ -236,19 +190,11 @@ class InterceptorOrderingSpec extends Specification {
response.status.code == 200
response.body().sessionData != null
response.body().sessionData.message == 'Session data from interceptor'
-
- cleanup:
- client.close()
}
// ========== Conditional Matching Tests ==========
def "test interceptor conditional matching - matched"() {
- given:
- def client = createClient()
- // Reset first
- client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), Map)
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/conditionalAction?match=yes'),
@@ -262,17 +208,9 @@ class InterceptorOrderingSpec extends Specification {
Map
)
orderResponse.body().executionOrder.contains('conditional:before:matched')
-
- cleanup:
- client.close()
}
def "test interceptor conditional matching - not matched"() {
- given:
- def client = createClient()
- // Reset first
- client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), Map)
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/conditionalAction?match=no'),
@@ -286,19 +224,11 @@ class InterceptorOrderingSpec extends Specification {
Map
)
orderResponse.body().executionOrder.contains('conditional:before:notmatched')
-
- cleanup:
- client.close()
}
// ========== Timing Interceptor Tests ==========
def "test timing interceptor tracks request duration"() {
- given:
- def client = createClient()
- // Reset first
- client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), Map)
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/slowAction?delay=50'),
@@ -317,19 +247,12 @@ class InterceptorOrderingSpec extends Specification {
)
orderResponse.body().executionOrder.any { it.startsWith('timing:before') }
orderResponse.body().executionOrder.any { it.startsWith('timing:after') }
-
- cleanup:
- client.close()
}
// ========== Multiple Requests Tests ==========
def "test interceptors work correctly across multiple requests"() {
- given:
- def client = createClient()
-
when: "make multiple requests"
- client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), Map)
def response1 = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/index'),
Map
@@ -349,17 +272,11 @@ class InterceptorOrderingSpec extends Specification {
// Both should have similar execution patterns
response1.body().executionOrder.contains('first:before')
response2.body().executionOrder.contains('first:before')
-
- cleanup:
- client.close()
}
// ========== Data Action Tests ==========
def "test interceptors work with data actions"() {
- given:
- def client = createClient()
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/dataAction?data=testValue'),
@@ -369,18 +286,11 @@ class InterceptorOrderingSpec extends Specification {
then:
response.status.code == 200
response.body().data == 'testValue'
-
- cleanup:
- client.close()
}
// ========== Execution Order Verification ==========
def "test complete before-controller-after sequence"() {
- given:
- def client = createClient()
- client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), Map)
-
when:
def response = client.toBlocking().exchange(
HttpRequest.GET('/interceptorTest/index'),
@@ -400,8 +310,5 @@ class InterceptorOrderingSpec extends Specification {
beforeEntries.every { order.indexOf(it) < order.indexOf(controllerEntry) }
// All afters come last
afterEntries.every { order.indexOf(it) > order.indexOf(controllerEntry) }
-
- cleanup:
- client.close()
}
}
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/requestresponse/RequestResponseSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/requestresponse/RequestResponseSpec.groovy
index 40f907bbad..d2c44d6c78 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/requestresponse/RequestResponseSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/requestresponse/RequestResponseSpec.groovy
@@ -18,36 +18,34 @@
*/
package functionaltests.requestresponse
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
import groovy.json.JsonSlurper
+
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
-import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.cookie.Cookie
-import spock.lang.Specification
import spock.lang.Shared
+import spock.lang.Specification
+
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for request/response handling patterns including
* headers, cookies, session management, and request attributes.
*/
-@Integration(applicationClass = Application)
-@Rollback
+@Integration
class RequestResponseSpec extends Specification {
@Shared
HttpClient client
def setup() {
- client = HttpClient.create(new URL("http://localhost:${serverPort}"))
+ client = client ?: HttpClient.create(new URL("http://localhost:${serverPort}"))
}
- def cleanup() {
- client?.close()
+ def cleanupSpec() {
+ client.close()
}
/**
@@ -63,7 +61,7 @@ class RequestResponseSpec extends Specification {
def "echo request headers returns all headers"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/echoHeaders')
.header('X-Custom-Header', 'TestValue')
.header('X-Another-Header', 'AnotherValue'),
@@ -79,7 +77,7 @@ class RequestResponseSpec extends Specification {
def "get specific header returns correct value"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/getSpecificHeader?headerName=X-Test-Header')
.header('X-Test-Header', 'MyTestValue'),
String
@@ -94,7 +92,7 @@ class RequestResponseSpec extends Specification {
def "check accept header detects JSON accept type"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/checkAcceptHeader')
.accept('application/json'),
String
@@ -110,7 +108,7 @@ class RequestResponseSpec extends Specification {
def "set custom headers returns headers in response"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setCustomHeaders'),
String
)
@@ -124,7 +122,7 @@ class RequestResponseSpec extends Specification {
def "set cache headers configures caching properly"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setCacheHeaders'),
String
)
@@ -138,7 +136,7 @@ class RequestResponseSpec extends Specification {
def "set no-cache headers prevents caching"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setNoCacheHeaders'),
String
)
@@ -152,7 +150,7 @@ class RequestResponseSpec extends Specification {
def "set content disposition for file download"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setContentDisposition'),
String
)
@@ -164,7 +162,7 @@ class RequestResponseSpec extends Specification {
def "set multiple custom headers returns all headers"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setMultipleCustomHeaders'),
String
)
@@ -180,7 +178,7 @@ class RequestResponseSpec extends Specification {
def "set cookie returns Set-Cookie header"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setCookie?name=myCookie&value=myValue'),
String
)
@@ -196,7 +194,7 @@ class RequestResponseSpec extends Specification {
def "set multiple cookies returns multiple Set-Cookie headers"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setMultipleCookies'),
String
)
@@ -210,7 +208,7 @@ class RequestResponseSpec extends Specification {
def "get cookies reads cookies from request"() {
when:
- MutableHttpRequest<Object> request = HttpRequest.GET('/requestResponseTest/getCookies')
+ def request = HttpRequest.GET('/requestResponseTest/getCookies')
.cookie(Cookie.of('testCookie1', 'value1'))
.cookie(Cookie.of('testCookie2', 'value2'))
HttpResponse<String> response = client.toBlocking().exchange(request, String)
@@ -224,7 +222,7 @@ class RequestResponseSpec extends Specification {
def "get specific cookie returns correct cookie value"() {
when:
- MutableHttpRequest<Object> request = HttpRequest.GET('/requestResponseTest/getSpecificCookie?name=myCookie')
+ def request = HttpRequest.GET('/requestResponseTest/getSpecificCookie?name=myCookie')
.cookie(Cookie.of('myCookie', 'cookieValue'))
HttpResponse<String> response = client.toBlocking().exchange(request, String)
def json = new JsonSlurper().parseText(response.body())
@@ -238,7 +236,7 @@ class RequestResponseSpec extends Specification {
def "delete cookie sets max-age to 0"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/deleteCookie?name=deletedCookie'),
String
)
@@ -254,7 +252,7 @@ class RequestResponseSpec extends Specification {
def "set session attribute stores value in session"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setSessionAttribute?key=testKey&value=testValue'),
String
)
@@ -270,7 +268,7 @@ class RequestResponseSpec extends Specification {
def "get session attribute retrieves stored value"() {
given:
// First set a session attribute
- HttpResponse<String> setResponse = client.toBlocking().exchange(
+ def setResponse = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setSessionAttribute?key=retrieveKey&value=retrieveValue'),
String
)
@@ -278,12 +276,12 @@ class RequestResponseSpec extends Specification {
when:
// Then retrieve it with the same session
- MutableHttpRequest<Object> getRequest = HttpRequest.GET('/requestResponseTest/getSessionAttribute?key=retrieveKey')
+ def getRequest = HttpRequest.GET('/requestResponseTest/getSessionAttribute?key=retrieveKey')
if (sessionCookie) {
def cookieValue = sessionCookie.split(';')[0]
getRequest = getRequest.header('Cookie', cookieValue)
}
- HttpResponse<String> response = client.toBlocking().exchange(getRequest, String)
+ def response = client.toBlocking().exchange(getRequest, String)
def json = new JsonSlurper().parseText(response.body())
then:
@@ -296,7 +294,7 @@ class RequestResponseSpec extends Specification {
def "session counter increments on each request"() {
given:
// First request
- HttpResponse<String> response1 = client.toBlocking().exchange(
+ def response1 = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/sessionCounter'),
String
)
@@ -305,12 +303,12 @@ class RequestResponseSpec extends Specification {
when:
// Second request with same session
- MutableHttpRequest<Object> request2 = HttpRequest.GET('/requestResponseTest/sessionCounter')
+ def request2 = HttpRequest.GET('/requestResponseTest/sessionCounter')
if (sessionCookie) {
def cookieValue = sessionCookie.split(';')[0]
request2 = request2.header('Cookie', cookieValue)
}
- HttpResponse<String> response2 = client.toBlocking().exchange(request2, String)
+ def response2 = client.toBlocking().exchange(request2, String)
def json2 = new JsonSlurper().parseText(response2.body())
then:
@@ -324,7 +322,7 @@ class RequestResponseSpec extends Specification {
def "get request info returns server details"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/getRequestInfo'),
String
)
@@ -340,7 +338,7 @@ class RequestResponseSpec extends Specification {
def "get request parameters returns query parameters"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/getRequestParameters?param1=value1¶m2=value2'),
String
)
@@ -354,7 +352,7 @@ class RequestResponseSpec extends Specification {
def "set request attribute stores and retrieves value"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/setRequestAttribute?key=myAttr&value=myVal'),
String
)
@@ -371,7 +369,7 @@ class RequestResponseSpec extends Specification {
def "unicode response returns characters in multiple languages"() {
when:
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/requestResponseTest/unicodeResponse'),
String
)
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/services/ServiceIntegrationSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/services/ServiceIntegrationSpec.groovy
index a2e414d48e..23b4a4f941 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/services/ServiceIntegrationSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/services/ServiceIntegrationSpec.groovy
@@ -16,23 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.services
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
-import grails.gorm.transactions.Rollback
-import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
-import spock.lang.Stepwise
+
+import org.springframework.beans.factory.annotation.Autowired
+
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for Grails services.
* Tests transactional behavior, rollback scenarios, async operations,
* and service dependency injection.
*/
-@Integration(applicationClass = Application)
-@Rollback
+@Integration
class ServiceIntegrationSpec extends Specification {
@Autowired
@@ -46,18 +44,22 @@ class ServiceIntegrationSpec extends Specification {
// ========== Transactional Service Tests ==========
+ @Rollback
def "service can add and retrieve product"() {
when: "adding a product through the service"
inventoryService.addProduct('SKU-001', 'Test Product', 29.99, 100)
- then: "product can be retrieved"
+ and: "retrieving the product"
def product = inventoryService.findBySku('SKU-001')
+
+ then: "the product was retrieved"
product != null
product.name == 'Test Product'
product.price == 29.99
product.quantity == 100
}
+ @Rollback
def "read-only transaction returns correct data"() {
given: "multiple products exist"
inventoryService.addProduct('CHEAP-001', 'Cheap Item', 5.00, 50)
@@ -72,6 +74,7 @@ class ServiceIntegrationSpec extends Specification {
expensiveItems[0].sku == 'EXPENSIVE-001'
}
+ // Do not use @Rollback here to test actual rollback behavior
def "runtime exception causes transaction rollback"() {
given: "initial count"
def initialCount = inventoryService.countAll()
@@ -81,13 +84,10 @@ class ServiceIntegrationSpec extends Specification {
then: "exception is thrown"
thrown(RuntimeException)
-
- // Note: Under @Rollback, the outer test transaction encompasses everything,
- // so we verify the exception was thrown (which marks the transaction for rollback).
- // The actual rollback happens at the end of the test method.
- // To verify the service's rollback behavior in isolation, we'd need REQUIRES_NEW.
+ initialCount == inventoryService.countAll()
}
+ @Rollback
def "checked exception does NOT rollback by default"() {
given: "initial count"
def initialCount = inventoryService.countAll()
@@ -101,21 +101,23 @@ class ServiceIntegrationSpec extends Specification {
and: "transaction is committed - product IS saved (default behavior)"
// Note: This test documents default Spring behavior
// Checked exceptions don't trigger rollback unless configured
- inventoryService.countAll() >= initialCount
+ inventoryService.countAll() > initialCount
}
+ // Do not use @Rollback here to test actual rollback behavior
def "rollbackFor configuration causes rollback on checked exception"() {
+ given: "initial count"
+ def initialCount = inventoryService.countAll()
+
when: "adding product with rollbackFor configured"
inventoryService.addProductRollbackOnCheckedException('ROLLBACK-001', 'Should Rollback', 20.00, 7)
then: "exception is thrown"
thrown(Exception)
-
- // Note: The rollbackFor annotation marks the transaction for rollback.
- // Under @Rollback test context, the actual rollback happens at end of test.
- // This test verifies the exception is properly thrown, which triggers the rollback.
+ inventoryService.countAll() == initialCount
}
+ @Rollback
def "update within transaction works correctly"() {
given: "an existing product"
inventoryService.addProduct('UPDATE-001', 'Update Test', 50.00, 100)
@@ -124,20 +126,22 @@ class ServiceIntegrationSpec extends Specification {
def result = inventoryService.updateQuantity('UPDATE-001', 75)
then: "update succeeds"
- result == true
+ result
and: "new quantity is persisted"
inventoryService.findBySku('UPDATE-001').quantity == 75
}
+ @Rollback
def "update non-existent product returns false"() {
when: "updating non-existent product"
def result = inventoryService.updateQuantity('NONEXISTENT', 50)
then: "update returns false"
- result == false
+ !result
}
+ @Rollback
def "multi-entity transaction commits atomically"() {
given: "two products with initial quantities"
inventoryService.addProduct('FROM-001', 'Source Product', 10.00, 100)
@@ -151,6 +155,7 @@ class ServiceIntegrationSpec extends Specification {
inventoryService.findBySku('TO-001').quantity == 80
}
+ @Rollback
def "multi-entity transaction rolls back on failure"() {
given: "two products"
inventoryService.addProduct('TRANSFER-FROM', 'Source', 10.00, 20)
@@ -167,6 +172,7 @@ class ServiceIntegrationSpec extends Specification {
inventoryService.findBySku('TRANSFER-TO').quantity == 10
}
+ @Rollback
def "batch insert processes multiple items"() {
given: "a list of items to insert"
def items = [
@@ -185,6 +191,7 @@ class ServiceIntegrationSpec extends Specification {
inventoryService.findBySku('BATCH-003') != null
}
+ @Rollback
def "delete removes product"() {
given: "an existing product"
inventoryService.addProduct('DELETE-001', 'To Delete', 5.00, 1)
@@ -194,12 +201,13 @@ class ServiceIntegrationSpec extends Specification {
def result = inventoryService.deleteProduct('DELETE-001')
then: "delete succeeds"
- result == true
+ result
and: "product no longer exists"
inventoryService.findBySku('DELETE-001') == null
}
+ @Rollback
def "total inventory value calculation"() {
given: "products with known values"
inventoryService.addProduct('VALUE-001', 'Item 1', 10.00, 5) // 50.00
@@ -221,6 +229,7 @@ class ServiceIntegrationSpec extends Specification {
orderService.applicationContext != null
}
+ @Rollback
def "order service uses inventory service"() {
given: "product in inventory"
inventoryService.addProduct('ORDER-001', 'Orderable Item', 49.99, 50)
@@ -239,6 +248,7 @@ class ServiceIntegrationSpec extends Specification {
inventoryService.findBySku('ORDER-001').quantity == 45
}
+ @Rollback
def "order fails for non-existent product"() {
when: "ordering non-existent product"
def result = orderService.placeOrder('NONEXISTENT', 1)
@@ -248,6 +258,7 @@ class ServiceIntegrationSpec extends Specification {
result.error == 'Product not found'
}
+ @Rollback
def "order fails for insufficient stock"() {
given: "product with limited stock"
inventoryService.addProduct('LIMITED-001', 'Limited Stock', 100.00, 3)
@@ -262,6 +273,7 @@ class ServiceIntegrationSpec extends Specification {
result.requested == 10
}
+ @Rollback
def "get quote returns pricing without modifying inventory"() {
given: "product in inventory"
inventoryService.addProduct('QUOTE-001', 'Quotable Item', 75.00, 20)
@@ -281,9 +293,9 @@ class ServiceIntegrationSpec extends Specification {
def "service can check bean availability"() {
expect: "known services are available"
- orderService.isServiceAvailable('inventoryService') == true
- orderService.isServiceAvailable('asyncProcessingService') == true
- orderService.isServiceAvailable('nonExistentService') == false
+ orderService.isServiceAvailable('inventoryService')
+ orderService.isServiceAvailable('asyncProcessingService')
+ !orderService.isServiceAvailable('nonExistentService')
}
def "service can retrieve beans dynamically"() {
@@ -325,6 +337,7 @@ class ServiceIntegrationSpec extends Specification {
result == ['cba', 'fed', 'ihg']
}
+ @Rollback
def "async method can access database"() {
given: "products in database"
inventoryService.addProduct('ASYNC-001', 'Async Test 1', 10.00, 5)
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/springevents/SpringEventsSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/springevents/SpringEventsSpec.groovy
index 01262944bf..94fe5deb5e 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/springevents/SpringEventsSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/springevents/SpringEventsSpec.groovy
@@ -16,28 +16,28 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.springevents
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import groovy.json.JsonSlurper
+
import io.micronaut.http.HttpRequest
-import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
-import io.micronaut.http.MediaType
import io.micronaut.http.client.HttpClient
-import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Narrative
+import spock.lang.Shared
import spock.lang.Specification
+import org.springframework.beans.factory.annotation.Autowired
+
+import grails.testing.mixin.integration.Integration
+
/**
* Integration tests for Spring application events in Grails.
*
* Tests event publishing, listening, conditional handling,
* ordered listeners, and event payloads.
*/
-@Integration(applicationClass = Application)
+@Integration
@Narrative('''
Spring's ApplicationEvent mechanism allows decoupled communication between
components. Grails integrates with Spring events via @EventListener annotations
@@ -51,31 +51,35 @@ class SpringEventsSpec extends Specification {
@Autowired
EventPublisherService eventPublisherService
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
- }
+ @Shared
+ HttpClient client
def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
eventListenerService.clearEvents()
}
+ def cleanupSpec() {
+ client.close()
+ }
+
// ========== Basic Event Publishing ==========
def "custom event is published and received"() {
when: "publishing a custom event"
- eventPublisherService.publishCustomEvent("Test Message", [key: "value"])
+ eventPublisherService.publishCustomEvent('Test Message', [key: 'value'])
then: "event is received by listener"
eventListenerService.eventCount == 1
eventListenerService.customEvents.size() == 1
- eventListenerService.customEvents[0].message == "Test Message"
- eventListenerService.customEvents[0].payload.key == "value"
+ eventListenerService.customEvents[0].message == 'Test Message'
+ eventListenerService.customEvents[0].payload.key == 'value'
}
def "event createdAt is set"() {
when: "publishing an event"
def before = new Date()
- eventPublisherService.publishCustomEvent("Timestamp Test")
+ eventPublisherService.publishCustomEvent('Timestamp Test')
def after = new Date()
then: "createdAt is within expected range"
@@ -85,13 +89,13 @@ class SpringEventsSpec extends Specification {
def "user action event is published and received"() {
when: "publishing a user action event"
- eventPublisherService.publishUserAction("user-123", "LOGIN", [ip: "192.168.1.1"])
+ eventPublisherService.publishUserAction('user-123', 'LOGIN', [ip: '192.168.1.1'])
then: "event is received with correct data"
eventListenerService.userActionEvents.size() == 1
- eventListenerService.userActionEvents[0].userId == "user-123"
- eventListenerService.userActionEvents[0].action == "LOGIN"
- eventListenerService.userActionEvents[0].metadata.ip == "192.168.1.1"
+ eventListenerService.userActionEvents[0].userId == 'user-123'
+ eventListenerService.userActionEvents[0].action == 'LOGIN'
+ eventListenerService.userActionEvents[0].metadata.ip == '192.168.1.1'
}
// ========== Multiple Events ==========
@@ -105,16 +109,16 @@ class SpringEventsSpec extends Specification {
eventListenerService.customEvents.size() == 5
and: "events have correct sequence"
- eventListenerService.customEvents[0].message == "Event #1"
- eventListenerService.customEvents[4].message == "Event #5"
+ eventListenerService.customEvents[0].message == 'Event #1'
+ eventListenerService.customEvents[4].message == 'Event #5'
}
def "events from different types are tracked separately"() {
when: "publishing different event types"
- eventPublisherService.publishCustomEvent("Custom 1")
- eventPublisherService.publishUserAction("user1", "ACTION1")
- eventPublisherService.publishCustomEvent("Custom 2")
- eventPublisherService.publishUserAction("user2", "ACTION2")
+ eventPublisherService.publishCustomEvent('Custom 1')
+ eventPublisherService.publishUserAction('user1', 'ACTION1')
+ eventPublisherService.publishCustomEvent('Custom 2')
+ eventPublisherService.publishUserAction('user2', 'ACTION2')
then: "events are tracked by type"
eventListenerService.customEvents.size() == 2
@@ -126,41 +130,38 @@ class SpringEventsSpec extends Specification {
def "event listeners are executed in order"() {
when: "publishing a priority event"
- eventPublisherService.publishPriorityEvent(1, "test")
+ eventPublisherService.publishPriorityEvent(1, 'test')
then: "listeners execute in @Order sequence"
eventListenerService.orderedResults.size() == 3
- eventListenerService.orderedResults[0] == "first-test"
- eventListenerService.orderedResults[1] == "second-test"
- eventListenerService.orderedResults[2] == "third-test"
+ eventListenerService.orderedResults[0] == 'first-test'
+ eventListenerService.orderedResults[1] == 'second-test'
+ eventListenerService.orderedResults[2] == 'third-test'
}
// ========== Conditional Event Listeners ==========
def "conditional listener handles matching events"() {
when: "publishing event that matches condition"
- eventPublisherService.publishCustomEvent("IMPORTANT: Critical Alert", [:])
+ eventPublisherService.publishCustomEvent('IMPORTANT: Critical Alert', [:])
then: "conditional handler is triggered"
- eventListenerService.conditionalResults.any { it.startsWith("CONDITIONAL:IMPORTANT") }
+ eventListenerService.conditionalResults.any { it.startsWith('CONDITIONAL:IMPORTANT') }
}
def "conditional listener ignores non-matching events"() {
when: "publishing event that doesn't match condition"
- eventPublisherService.publishCustomEvent("NORMAL: Regular message", [:])
+ eventPublisherService.publishCustomEvent('NORMAL: Regular message', [:])
then: "conditional handler is not triggered"
- !eventListenerService.conditionalResults.any { it.startsWith("CONDITIONAL:") }
+ !eventListenerService.conditionalResults.any { it.startsWith('CONDITIONAL:') }
}
// ========== Event Publishing via HTTP ==========
def "event can be published via HTTP endpoint"() {
- given:
- def client = createClient()
-
when: "calling publish endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/publishCustom?message=HTTP+Event'),
String
)
@@ -169,18 +170,12 @@ class SpringEventsSpec extends Specification {
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
json.published == true
- json.message == "HTTP Event"
-
- cleanup:
- client.close()
+ json.message == 'HTTP Event'
}
def "user action event can be published via HTTP"() {
- given:
- def client = createClient()
-
when: "calling user action publish endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/publishUserAction?userId=web-user&userAction=CLICK'),
String
)
@@ -189,19 +184,13 @@ class SpringEventsSpec extends Specification {
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
json.published == true
- json.userId == "web-user"
- json.userAction == "CLICK"
-
- cleanup:
- client.close()
+ json.userId == 'web-user'
+ json.userAction == 'CLICK'
}
def "priority event ordering works via HTTP"() {
- given:
- def client = createClient()
-
when: "calling priority publish endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/publishPriority?data=http-test'),
String
)
@@ -210,18 +199,12 @@ class SpringEventsSpec extends Specification {
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
json.orderedResults.size() == 3
- json.orderedResults[0] == "first-http-test"
-
- cleanup:
- client.close()
+ json.orderedResults[0] == 'first-http-test'
}
def "multiple events can be published via HTTP"() {
- given:
- def client = createClient()
-
when: "calling multiple publish endpoint"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/publishMultiple?count=3'),
String
)
@@ -231,17 +214,11 @@ class SpringEventsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.count == 3
json.receivedCount == 3
-
- cleanup:
- client.close()
}
def "conditional event works via HTTP for matching message"() {
- given:
- def client = createClient()
-
when: "publishing important message"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/publishConditional?important=true'),
String
)
@@ -249,18 +226,12 @@ class SpringEventsSpec extends Specification {
then: "conditional handler triggers"
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
- json.conditionalResults.any { it.startsWith("CONDITIONAL:") }
-
- cleanup:
- client.close()
+ json.conditionalResults.any { it.startsWith('CONDITIONAL:') }
}
def "conditional event skipped via HTTP for non-matching message"() {
- given:
- def client = createClient()
-
when: "publishing normal message"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/publishConditional?important=false'),
String
)
@@ -268,23 +239,19 @@ class SpringEventsSpec extends Specification {
then: "conditional handler does not trigger"
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
- !json.conditionalResults.any { it.toString().startsWith("CONDITIONAL:") }
-
- cleanup:
- client.close()
+ !json.conditionalResults.any { it.toString().startsWith('CONDITIONAL:') }
}
// ========== Event Stats ==========
def "event stats are accessible via HTTP"() {
given:
- def client = createClient()
- eventPublisherService.publishCustomEvent("Stats Test 1")
- eventPublisherService.publishCustomEvent("Stats Test 2")
- eventPublisherService.publishUserAction("user", "action")
+ eventPublisherService.publishCustomEvent('Stats Test 1')
+ eventPublisherService.publishCustomEvent('Stats Test 2')
+ eventPublisherService.publishUserAction('user', 'action')
when: "getting stats"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/stats'),
String
)
@@ -295,20 +262,16 @@ class SpringEventsSpec extends Specification {
json.totalEvents == 3
json.customEvents == 2
json.userActionEvents == 1
-
- cleanup:
- client.close()
}
// ========== Clear Events ==========
def "events can be cleared via HTTP"() {
given:
- def client = createClient()
- eventPublisherService.publishCustomEvent("To be cleared")
+ eventPublisherService.publishCustomEvent('To be cleared')
when: "clearing events"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/clearEvents'),
String
)
@@ -316,19 +279,13 @@ class SpringEventsSpec extends Specification {
then: "events are cleared"
response.status == HttpStatus.OK
eventListenerService.eventCount == 0
-
- cleanup:
- client.close()
}
// ========== Transactional Event Publishing ==========
def "event can be published in transactional context"() {
- given:
- def client = createClient()
-
when: "publishing transactional event"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/springEvent/publishTransactional?message=tx-test'),
String
)
@@ -339,10 +296,7 @@ class SpringEventsSpec extends Specification {
json.published == true
and: "event is received"
- eventListenerService.customEvents.any { it.message.contains("tx-test") }
-
- cleanup:
- client.close()
+ eventListenerService.customEvents.any { it.message.contains('tx-test') }
}
// ========== Event Payload Tests ==========
@@ -354,22 +308,22 @@ class SpringEventsSpec extends Specification {
list: [1, 2, 3],
map: [a: 'b', c: 'd']
],
- string: "value",
+ string: 'value',
number: 42
]
- eventPublisherService.publishCustomEvent("Complex Payload", payload)
+ eventPublisherService.publishCustomEvent('Complex Payload', payload)
then: "payload is preserved"
def event = eventListenerService.customEvents[0]
event.payload.nested.list == [1, 2, 3]
event.payload.nested.map.a == 'b'
- event.payload.string == "value"
+ event.payload.string == 'value'
event.payload.number == 42
}
def "event payload handles null values"() {
when: "publishing event with null in payload"
- eventPublisherService.publishCustomEvent("Null Payload", [key: null])
+ eventPublisherService.publishCustomEvent('Null Payload', [key: null])
then: "null is preserved"
def event = eventListenerService.customEvents[0]
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/taglib/TagLibSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/taglib/TagLibSpec.groovy
index 419f82860b..d81c17169f 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/taglib/TagLibSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/taglib/TagLibSpec.groovy
@@ -16,74 +16,66 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.taglib
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
+import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
+import grails.testing.mixin.integration.Integration
+
/**
* Integration tests for GSP Tag Libraries.
* Tests both custom tag libraries and built-in Grails tags.
*/
-@Integration(applicationClass = Application)
+@Integration
class TagLibSpec extends Specification {
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
+ @Shared
+ HttpClient client
+
+ def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
+ }
+
+ def cleanupSpec() {
+ client.close()
}
// ========== Custom Tag: hello ==========
def "custom:hello tag renders greeting with name attribute"() {
- given: "a request with a name parameter"
- def client = createClient()
-
when: "calling the hello tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testHelloTag?name=Grails"),
+ HttpRequest.GET('/tagLibTest/testHelloTag?name=Grails'),
String
)
then: "greeting is rendered with the name"
response.status.code == 200
response.body().contains('Hello, Grails!')
-
- cleanup:
- client.close()
}
def "custom:hello tag uses default name when not provided"() {
- given: "a request without a name parameter"
- def client = createClient()
-
when: "calling the hello tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testHelloTag"),
+ HttpRequest.GET('/tagLibTest/testHelloTag'),
String
)
then: "greeting is rendered with default name"
response.status.code == 200
response.body().contains('Hello, World!')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: wrapper ==========
def "custom:wrapper tag renders title and body content"() {
- given: "a request with title and content"
- def client = createClient()
-
when: "calling the wrapper tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testWrapperTag?title=My%20Section&content=Section%20content"),
+ HttpRequest.GET('/tagLibTest/testWrapperTag?title=My%20Section&content=Section%20content'),
String
)
@@ -93,38 +85,26 @@ class TagLibSpec extends Specification {
body.contains('<div class="wrapper">')
body.contains('<h2>My Section</h2>')
body.contains('Section content')
-
- cleanup:
- client.close()
}
def "custom:wrapper tag applies custom CSS class"() {
- given: "a request with custom CSS class"
- def client = createClient()
-
when: "calling the wrapper tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testWrapperTag?title=Test&content=Test&cssClass=custom-wrapper"),
+ HttpRequest.GET('/tagLibTest/testWrapperTag?title=Test&content=Test&cssClass=custom-wrapper'),
String
)
then: "custom CSS class is applied"
response.status.code == 200
response.body().contains('<div class="custom-wrapper">')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: iterate ==========
def "custom:iterate tag iterates over items"() {
- given: "a request with items"
- def client = createClient()
-
when: "calling the iterate tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testIterateTag?items=A,B,C"),
+ HttpRequest.GET('/tagLibTest/testIterateTag?items=A,B,C'),
String
)
@@ -134,18 +114,12 @@ class TagLibSpec extends Specification {
body.contains('Item: A')
body.contains('Item: B')
body.contains('Item: C')
-
- cleanup:
- client.close()
}
def "custom:iterate tag uses separator between items"() {
- given: "a request with separator"
- def client = createClient()
-
when: "calling the iterate tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testIterateTag?items=X,Y,Z&separator=-"),
+ HttpRequest.GET('/tagLibTest/testIterateTag?items=X,Y,Z&separator=-'),
String
)
@@ -153,20 +127,14 @@ class TagLibSpec extends Specification {
response.status.code == 200
def body = response.body()
body.contains('Item: X-Item: Y-Item: Z')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: showIf/hideIf ==========
def "custom:showIf tag shows content when condition is true"() {
- given: "a request with true condition"
- def client = createClient()
-
when: "calling the conditional tags test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testConditionalTags?condition=true"),
+ HttpRequest.GET('/tagLibTest/testConditionalTags?condition=true'),
String
)
@@ -175,18 +143,12 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('id="showIf-result">VISIBLE')
!body.contains('id="hideIf-result">HIDDEN')
-
- cleanup:
- client.close()
}
def "custom:hideIf tag shows content when condition is false"() {
- given: "a request with false condition"
- def client = createClient()
-
when: "calling the conditional tags test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testConditionalTags?condition=false"),
+ HttpRequest.GET('/tagLibTest/testConditionalTags?condition=false'),
String
)
@@ -195,18 +157,12 @@ class TagLibSpec extends Specification {
def body = response.body()
!body.contains('id="showIf-result">VISIBLE')
body.contains('id="hideIf-result">HIDDEN')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: formatted ==========
@Unroll
def "custom:formatted tag formats value as #format"() {
- given: "a request with format parameters"
- def client = createClient()
-
when: "calling the formatted tag test endpoint"
def response = client.toBlocking().exchange(
HttpRequest.GET("/tagLibTest/testFormattedTag?value=${value}&format=${format}&decimals=${decimals}"),
@@ -217,9 +173,6 @@ class TagLibSpec extends Specification {
response.status.code == 200
response.body().contains(expected)
- cleanup:
- client.close()
-
where:
value | format | decimals | expected
100.0 | 'currency' | 2 | '$100.00'
@@ -230,12 +183,9 @@ class TagLibSpec extends Specification {
// ========== Custom Tag: list ==========
def "custom:list tag renders unordered list by default"() {
- given: "a request with items"
- def client = createClient()
-
when: "calling the list tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testListTag?items=Apple,Banana,Cherry"),
+ HttpRequest.GET('/tagLibTest/testListTag?items=Apple,Banana,Cherry'),
String
)
@@ -247,18 +197,12 @@ class TagLibSpec extends Specification {
body.contains('<li>Banana</li>')
body.contains('<li>Cherry</li>')
body.contains('</ul>')
-
- cleanup:
- client.close()
}
def "custom:list tag renders ordered list when type is ordered"() {
- given: "a request with ordered type"
- def client = createClient()
-
when: "calling the list tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testListTag?items=First,Second,Third&type=ordered"),
+ HttpRequest.GET('/tagLibTest/testListTag?items=First,Second,Third&type=ordered'),
String
)
@@ -267,20 +211,14 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('<ol>')
body.contains('</ol>')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: panel ==========
def "custom:panel tag renders panel with title and body"() {
- given: "a request with panel parameters"
- def client = createClient()
-
when: "calling the panel tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testPanelTag?title=Info%20Panel&type=info&content=Panel%20content"),
+ HttpRequest.GET('/tagLibTest/testPanelTag?title=Info%20Panel&type=info&content=Panel%20content'),
String
)
@@ -292,36 +230,24 @@ class TagLibSpec extends Specification {
body.contains('<h3>Info Panel</h3>')
body.contains('class="panel-body"')
body.contains('Panel content')
-
- cleanup:
- client.close()
}
def "custom:panel tag renders collapse button when collapsible"() {
- given: "a request with collapsible panel"
- def client = createClient()
-
when: "calling the panel tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testPanelTag?title=Collapsible&collapsible=true"),
+ HttpRequest.GET('/tagLibTest/testPanelTag?title=Collapsible&collapsible=true'),
String
)
then: "collapse button is rendered"
response.status.code == 200
response.body().contains('class="collapse-btn"')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: badge ==========
@Unroll
def "custom:badge tag renders badge with type=#type and size=#size"() {
- given: "a request with badge parameters"
- def client = createClient()
-
when: "calling the badge tag test endpoint"
def response = client.toBlocking().exchange(
HttpRequest.GET("/tagLibTest/testBadgeTag?type=${type}&size=${size}&content=${content}"),
@@ -335,9 +261,6 @@ class TagLibSpec extends Specification {
body.contains("badge-${size}")
body.contains(">${content}<")
- cleanup:
- client.close()
-
where:
type | size | content
'success' | 'small' | '10'
@@ -348,12 +271,9 @@ class TagLibSpec extends Specification {
// ========== Custom Tag: progress ==========
def "custom:progress tag renders progress bar with percentage"() {
- given: "a request with progress parameters"
- def client = createClient()
-
when: "calling the progress tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testProgressTag?value=75&max=100"),
+ HttpRequest.GET('/tagLibTest/testProgressTag?value=75&max=100'),
String
)
@@ -364,18 +284,12 @@ class TagLibSpec extends Specification {
body.contains('class="progress-bar"')
body.contains('style="width: 75%"')
body.contains('75%')
-
- cleanup:
- client.close()
}
def "custom:progress tag hides label when showLabel is false"() {
- given: "a request with showLabel=false"
- def client = createClient()
-
when: "calling the progress tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testProgressTag?value=50&max=100&showLabel=false"),
+ HttpRequest.GET('/tagLibTest/testProgressTag?value=50&max=100&showLabel=false'),
String
)
@@ -384,20 +298,14 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('class="progress-bar"')
!body.contains('>50%<')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: repeat ==========
def "custom:repeat tag repeats body content specified times"() {
- given: "a request with times parameter"
- def client = createClient()
-
when: "calling the repeat tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testRepeatTag?times=3"),
+ HttpRequest.GET('/tagLibTest/testRepeatTag?times=3'),
String
)
@@ -407,58 +315,40 @@ class TagLibSpec extends Specification {
body.contains('Repeat #1')
body.contains('Repeat #2')
body.contains('Repeat #3')
-
- cleanup:
- client.close()
}
def "custom:repeat tag uses separator between repetitions"() {
- given: "a request with separator"
- def client = createClient()
-
when: "calling the repeat tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testRepeatTag?times=2&separator=%20-%20"),
+ HttpRequest.GET('/tagLibTest/testRepeatTag?times=2&separator=%20-%20'),
String
)
then: "repetitions are separated"
response.status.code == 200
response.body().contains('Repeat #1 - Repeat #2')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: raw ==========
def "custom:raw tag outputs unescaped HTML content"() {
- given: "a request with HTML content"
- def client = createClient()
-
when: "calling the raw tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testRawTag"),
+ HttpRequest.GET('/tagLibTest/testRawTag'),
String
)
then: "HTML content is not escaped"
response.status.code == 200
response.body().contains('<strong>Bold Text</strong>')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: definitionList ==========
def "custom:definitionList tag renders definition list from map"() {
- given: "a request to test definition list"
- def client = createClient()
-
when: "calling the definition list tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testDefinitionListTag"),
+ HttpRequest.GET('/tagLibTest/testDefinitionListTag'),
String
)
@@ -471,78 +361,54 @@ class TagLibSpec extends Specification {
body.contains('<dt>age</dt>')
body.contains('<dd>30</dd>')
body.contains('</dl>')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: requestInfo ==========
def "custom:requestInfo tag retrieves request attributes"() {
- given: "a request to test request info"
- def client = createClient()
-
when: "calling the request info tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testRequestInfoTag?attr=method"),
+ HttpRequest.GET('/tagLibTest/testRequestInfoTag?attr=method'),
String
)
then: "request attribute is output"
response.status.code == 200
response.body().contains('GET')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: sessionValue ==========
def "custom:sessionValue tag displays default when session value not set"() {
- given: "a request without session value"
- def client = createClient()
-
when: "calling the session value tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testSessionValueTag?key=nonexistent&default=DefaultUser"),
+ HttpRequest.GET('/tagLibTest/testSessionValueTag?key=nonexistent&default=DefaultUser'),
String
)
then: "default value is displayed"
response.status.code == 200
response.body().contains('DefaultUser')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: setVar ==========
def "custom:setVar tag sets pageScope variable"() {
- given: "a request to set a variable"
- def client = createClient()
-
when: "calling the setVar tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testSetVarTag?varName=testVar&varValue=TestValue"),
+ HttpRequest.GET('/tagLibTest/testSetVarTag?varName=testVar&varValue=TestValue'),
String
)
then: "variable is set and accessible"
response.status.code == 200
response.body().contains('Variable testVar = TestValue')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: alert ==========
@Unroll
def "custom:alert tag renders #type alert"() {
- given: "a request with alert parameters"
- def client = createClient()
-
when: "calling the alert tag test endpoint"
def response = client.toBlocking().exchange(
HttpRequest.GET("/tagLibTest/testAlertTag?type=${type}&message=${URLEncoder.encode(message, 'UTF-8')}"),
@@ -555,9 +421,6 @@ class TagLibSpec extends Specification {
body.contains("alert-${type}")
body.contains(message)
- cleanup:
- client.close()
-
where:
type | message
'info' | 'InfoMessage'
@@ -567,12 +430,9 @@ class TagLibSpec extends Specification {
}
def "custom:alert tag renders dismissible button when dismissible=true"() {
- given: "a request with dismissible alert"
- def client = createClient()
-
when: "calling the alert tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testAlertTag?type=info&dismissible=true"),
+ HttpRequest.GET('/tagLibTest/testAlertTag?type=info&dismissible=true'),
String
)
@@ -581,40 +441,28 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('alert-dismissible')
body.contains('class="close"')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: join ==========
def "custom:join tag joins items with separator"() {
- given: "a request with items to join"
- def client = createClient()
-
when: "calling the join tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testJoinTag?items=red,green,blue&separator=-"),
+ HttpRequest.GET('/tagLibTest/testJoinTag?items=red,green,blue&separator=-'),
String
)
then: "items are joined with separator"
response.status.code == 200
response.body().contains('red-green-blue')
-
- cleanup:
- client.close()
}
// ========== Custom Tag: cssClass ==========
def "custom:cssClass tag builds class string from boolean attributes"() {
- given: "a request with CSS class parameters"
- def client = createClient()
-
when: "calling the cssClass tag test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testCssClassTag?base=btn&active=true&disabled=false&highlighted=true"),
+ HttpRequest.GET('/tagLibTest/testCssClassTag?base=btn&active=true&disabled=false&highlighted=true'),
String
)
@@ -625,20 +473,14 @@ class TagLibSpec extends Specification {
body.contains('active')
body.contains('highlighted')
!body.contains('disabled')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:if ==========
def "g:if tag shows content when condition is true"() {
- given: "a request with a value greater than 5"
- def client = createClient()
-
when: "calling the built-in if test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInIf?value=10"),
+ HttpRequest.GET('/tagLibTest/testBuiltInIf?value=10'),
String
)
@@ -647,38 +489,26 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('Greater than 5')
!body.contains('Less than 5')
-
- cleanup:
- client.close()
}
def "g:elseif and g:else work correctly"() {
- given: "a request with a value of 30"
- def client = createClient()
-
when: "calling the built-in if test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInIf?value=30"),
+ HttpRequest.GET('/tagLibTest/testBuiltInIf?value=30'),
String
)
then: "elseif content is shown"
response.status.code == 200
response.body().contains('Over 20')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:each ==========
def "g:each tag iterates over collection"() {
- given: "a request with items"
- def client = createClient()
-
when: "calling the built-in each test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInEach?items=A,B,C"),
+ HttpRequest.GET('/tagLibTest/testBuiltInEach?items=A,B,C'),
String
)
@@ -688,18 +518,12 @@ class TagLibSpec extends Specification {
body.contains('[A]')
body.contains('[B]')
body.contains('[C]')
-
- cleanup:
- client.close()
}
def "g:each tag provides status variable"() {
- given: "a request with items"
- def client = createClient()
-
when: "calling the built-in each test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInEach?items=X,Y,Z"),
+ HttpRequest.GET('/tagLibTest/testBuiltInEach?items=X,Y,Z'),
String
)
@@ -709,20 +533,14 @@ class TagLibSpec extends Specification {
body.contains('0:X')
body.contains('1:Y')
body.contains('2:Z')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:collect ==========
def "g:collect tag transforms items"() {
- given: "a request with items"
- def client = createClient()
-
when: "calling the built-in collect test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInCollect?items=apple,banana"),
+ HttpRequest.GET('/tagLibTest/testBuiltInCollect?items=apple,banana'),
String
)
@@ -731,20 +549,14 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('APPLE')
body.contains('BANANA')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:findAll ==========
def "g:findAll tag filters items"() {
- given: "a request with threshold"
- def client = createClient()
-
when: "calling the built-in findAll test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInFindAll?threshold=7"),
+ HttpRequest.GET('/tagLibTest/testBuiltInFindAll?threshold=7'),
String
)
@@ -755,20 +567,14 @@ class TagLibSpec extends Specification {
body.contains('9')
body.contains('10')
!body.contains('7 ')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:link ==========
def "g:link tag creates link with controller and action"() {
- given: "a request to create a link"
- def client = createClient()
-
when: "calling the built-in link test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInLink?targetController=book&targetAction=show&targetId=42&linkText=View"),
+ HttpRequest.GET('/tagLibTest/testBuiltInLink?targetController=book&targetAction=show&targetId=42&linkText=View'),
String
)
@@ -778,40 +584,28 @@ class TagLibSpec extends Specification {
body.contains('<a href=')
body.contains('/book/show/42')
body.contains('>View</a>')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:createLink ==========
def "g:createLink tag creates URL string"() {
- given: "a request to create a link URL"
- def client = createClient()
-
when: "calling the built-in createLink test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInCreateLink?targetController=book&targetAction=list"),
+ HttpRequest.GET('/tagLibTest/testBuiltInCreateLink?targetController=book&targetAction=list'),
String
)
then: "URL is rendered"
response.status.code == 200
response.body().contains('/book/list')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:form ==========
def "g:form tag creates form with action"() {
- given: "a request to create a form"
- def client = createClient()
-
when: "calling the built-in form test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInForm?targetController=book&targetAction=save"),
+ HttpRequest.GET('/tagLibTest/testBuiltInForm?targetController=book&targetAction=save'),
String
)
@@ -822,40 +616,28 @@ class TagLibSpec extends Specification {
body.contains('action=')
body.contains('/book/save')
body.contains('method="post"')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:message ==========
def "g:message tag renders message with default"() {
- given: "a request with message code"
- def client = createClient()
-
when: "calling the built-in message test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInMessage?code=nonexistent.key&default=Default%20Message"),
+ HttpRequest.GET('/tagLibTest/testBuiltInMessage?code=nonexistent.key&default=Default%20Message'),
String
)
then: "default message is rendered"
response.status.code == 200
response.body().contains('Default Message')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:formatDate ==========
def "g:formatDate tag formats date"() {
- given: "a request with date"
- def client = createClient()
-
when: "calling the built-in formatDate test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInFormatDate"),
+ HttpRequest.GET('/tagLibTest/testBuiltInFormatDate'),
String
)
@@ -863,20 +645,14 @@ class TagLibSpec extends Specification {
response.status.code == 200
// Just verify it rendered something in date format
response.body() =~ /\d{4}-\d{2}-\d{2}/
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:formatNumber ==========
def "g:formatNumber tag formats number"() {
- given: "a request with number"
- def client = createClient()
-
when: "calling the built-in formatNumber test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInFormatNumber?number=1234567.89"),
+ HttpRequest.GET('/tagLibTest/testBuiltInFormatNumber?number=1234567.89'),
String
)
@@ -884,20 +660,14 @@ class TagLibSpec extends Specification {
response.status.code == 200
// Should contain formatted number (locale-dependent)
response.body() =~ /1.*234.*567/
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:set ==========
def "g:set tag sets and updates variables"() {
- given: "a request to test variable setting"
- def client = createClient()
-
when: "calling the built-in set test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInSet?initialValue=First&newValue=Second"),
+ HttpRequest.GET('/tagLibTest/testBuiltInSet?initialValue=First&newValue=Second'),
String
)
@@ -906,80 +676,56 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('id="set-initial">First')
body.contains('id="set-updated">Second')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:join ==========
def "g:join tag joins items with delimiter"() {
- given: "a request with items"
- def client = createClient()
-
when: "calling the built-in join test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInJoin?items=red,green,blue&delimiter=%20-%20"),
+ HttpRequest.GET('/tagLibTest/testBuiltInJoin?items=red,green,blue&delimiter=%20-%20'),
String
)
then: "items are joined"
response.status.code == 200
response.body().contains('red - green - blue')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:include ==========
def "g:include tag includes content from another action"() {
- given: "a request to test include"
- def client = createClient()
-
when: "calling the built-in include test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInInclude?message=Test%20Message"),
+ HttpRequest.GET('/tagLibTest/testBuiltInInclude?message=Test%20Message'),
String
)
then: "included content is rendered"
response.status.code == 200
response.body().contains('Included content: Test Message')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:render ==========
def "g:render tag renders template with model"() {
- given: "a request to test template rendering"
- def client = createClient()
-
when: "calling the built-in render test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInRender?text=Hello%20Template"),
+ HttpRequest.GET('/tagLibTest/testBuiltInRender?text=Hello%20Template'),
String
)
then: "template is rendered"
response.status.code == 200
response.body().contains('Template content: Hello Template')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:while ==========
def "g:while tag loops while condition is true"() {
- given: "a request with count"
- def client = createClient()
-
when: "calling the built-in while test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInWhile?count=3"),
+ HttpRequest.GET('/tagLibTest/testBuiltInWhile?count=3'),
String
)
@@ -990,20 +736,14 @@ class TagLibSpec extends Specification {
body.contains('Count: 2')
body.contains('Count: 3')
!body.contains('Count: 4')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:uploadForm ==========
def "g:uploadForm tag creates multipart form"() {
- given: "a request to create upload form"
- def client = createClient()
-
when: "calling the built-in uploadForm test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInUploadForm"),
+ HttpRequest.GET('/tagLibTest/testBuiltInUploadForm'),
String
)
@@ -1012,20 +752,14 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('<form')
body.contains('enctype="multipart/form-data"')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:select ==========
def "g:select tag creates select element with options"() {
- given: "a request to create select"
- def client = createClient()
-
when: "calling the built-in select test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInSelect?selected=2"),
+ HttpRequest.GET('/tagLibTest/testBuiltInSelect?selected=2'),
String
)
@@ -1035,20 +769,14 @@ class TagLibSpec extends Specification {
body.contains('<select')
body.contains('<option')
body.contains('value="2" selected')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:radio ==========
def "g:radio tag creates radio buttons"() {
- given: "a request to create radio buttons"
- def client = createClient()
-
when: "calling the built-in radio test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInRadio?selected=Option%20B"),
+ HttpRequest.GET('/tagLibTest/testBuiltInRadio?selected=Option%20B'),
String
)
@@ -1057,20 +785,14 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('type="radio"')
body.contains('checked="checked"')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:checkBox ==========
def "g:checkBox tag creates checkbox"() {
- given: "a request to create checkbox"
- def client = createClient()
-
when: "calling the built-in checkbox test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInCheckBox?checked=true"),
+ HttpRequest.GET('/tagLibTest/testBuiltInCheckBox?checked=true'),
String
)
@@ -1079,20 +801,14 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('type="checkbox"')
body.contains('checked="checked"')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:textArea ==========
def "g:textArea tag creates textarea"() {
- given: "a request to create textarea"
- def client = createClient()
-
when: "calling the built-in textarea test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInTextArea?value=Test%20Content&rows=5&cols=40"),
+ HttpRequest.GET('/tagLibTest/testBuiltInTextArea?value=Test%20Content&rows=5&cols=40'),
String
)
@@ -1103,20 +819,14 @@ class TagLibSpec extends Specification {
body.contains('rows="5"')
body.contains('cols="40"')
body.contains('Test Content')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:textField ==========
def "g:textField tag creates text input"() {
- given: "a request to create text field"
- def client = createClient()
-
when: "calling the built-in textField test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInTextField?value=Test%20Value&maxlength=50"),
+ HttpRequest.GET('/tagLibTest/testBuiltInTextField?value=Test%20Value&maxlength=50'),
String
)
@@ -1126,40 +836,28 @@ class TagLibSpec extends Specification {
body.contains('type="text"')
body.contains('value="Test Value"')
body.contains('maxlength="50"')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:passwordField ==========
def "g:passwordField tag creates password input"() {
- given: "a request to create password field"
- def client = createClient()
-
when: "calling the built-in passwordField test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInPasswordField"),
+ HttpRequest.GET('/tagLibTest/testBuiltInPasswordField'),
String
)
then: "password field is rendered"
response.status.code == 200
response.body().contains('type="password"')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:hiddenField ==========
def "g:hiddenField tag creates hidden input"() {
- given: "a request to create hidden field"
- def client = createClient()
-
when: "calling the built-in hiddenField test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInHiddenField?value=secret-value"),
+ HttpRequest.GET('/tagLibTest/testBuiltInHiddenField?value=secret-value'),
String
)
@@ -1168,40 +866,28 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('type="hidden"')
body.contains('value="secret-value"')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:fieldValue ==========
def "g:fieldValue tag extracts bean field value"() {
- given: "a request with a bean"
- def client = createClient()
-
when: "calling the built-in fieldValue test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInFieldValue?field=title"),
+ HttpRequest.GET('/tagLibTest/testBuiltInFieldValue?field=title'),
String
)
then: "field value is extracted"
response.status.code == 200
response.body().contains('Grails in Action')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:sortableColumn ==========
def "g:sortableColumn tag creates sortable table header"() {
- given: "a request to create sortable columns"
- def client = createClient()
-
when: "calling the built-in sortableColumn test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInSortableColumn"),
+ HttpRequest.GET('/tagLibTest/testBuiltInSortableColumn'),
String
)
@@ -1211,20 +897,14 @@ class TagLibSpec extends Specification {
body.contains('<th')
body.contains('Title')
body.contains('Author')
-
- cleanup:
- client.close()
}
// ========== Built-in Tag: g:paginate ==========
def "g:paginate tag creates pagination links"() {
- given: "a request for pagination"
- def client = createClient()
-
when: "calling the built-in paginate test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testBuiltInPaginate?total=100&max=10&offset=0"),
+ HttpRequest.GET('/tagLibTest/testBuiltInPaginate?total=100&max=10&offset=0'),
String
)
@@ -1233,20 +913,14 @@ class TagLibSpec extends Specification {
def body = response.body()
// Pagination should contain some links
body.contains('class=')
-
- cleanup:
- client.close()
}
// ========== Complex/Combined Tests ==========
def "nested custom tags render correctly"() {
- given: "a request for nested tags"
- def client = createClient()
-
when: "calling the nested tags test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testNestedTags"),
+ HttpRequest.GET('/tagLibTest/testNestedTags'),
String
)
@@ -1255,18 +929,12 @@ class TagLibSpec extends Specification {
def body = response.body()
body.contains('<ul>')
body.contains('class="badge')
-
- cleanup:
- client.close()
}
def "tags work with complex model data"() {
- given: "a request with complex model"
- def client = createClient()
-
when: "calling the tags with model test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testTagsWithModel"),
+ HttpRequest.GET('/tagLibTest/testTagsWithModel'),
String
)
@@ -1280,18 +948,12 @@ class TagLibSpec extends Specification {
body.contains('Charlie')
body.contains('Admin')
body.contains('User')
-
- cleanup:
- client.close()
}
def "encoding tags properly escape content"() {
- given: "a request with content to encode"
- def client = createClient()
-
when: "calling the encoding tags test endpoint"
def response = client.toBlocking().exchange(
- HttpRequest.GET("/tagLibTest/testEncodingTags"),
+ HttpRequest.GET('/tagLibTest/testEncodingTags'),
String
)
@@ -1302,8 +964,5 @@ class TagLibSpec extends Specification {
body.contains('<script>')
// Raw content should preserve HTML
body.contains('id="raw-content">')
-
- cleanup:
- client.close()
}
}
diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/urlmappings/UrlMappingsSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/urlmappings/UrlMappingsSpec.groovy
index 894214b069..c1e7603912 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/urlmappings/UrlMappingsSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/urlmappings/UrlMappingsSpec.groovy
@@ -16,46 +16,49 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package functionaltests.urlmappings
-import functionaltests.Application
-import grails.testing.mixin.integration.Integration
import groovy.json.JsonSlurper
-import io.micronaut.http.HttpMethod
+
import io.micronaut.http.HttpRequest
-import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import spock.lang.Narrative
+import spock.lang.Shared
import spock.lang.Specification
+import grails.testing.mixin.integration.Integration
+
/**
* Integration tests for Grails URL mappings features.
*
* Tests static paths, path variables, constraints, HTTP method mappings,
* redirects, and various URL mapping patterns.
*/
-@Integration(applicationClass = Application)
+@Integration
@Narrative('''
Grails URL mappings provide flexible routing of HTTP requests to controller actions.
This includes path variables, constraints, HTTP method-based routing, and redirects.
''')
class UrlMappingsSpec extends Specification {
- private HttpClient createClient() {
- HttpClient.create(new URL("http://localhost:$serverPort"))
+ @Shared
+ HttpClient client
+
+ def setup() {
+ client = client ?: HttpClient.create(new URL("http://localhost:$serverPort"))
+ }
+
+ def cleanupSpec() {
+ client.close()
}
// ========== Static Path Mappings ==========
def "static path mapping routes to correct action"() {
- given:
- def client = createClient()
-
when: "accessing static path"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/test'),
String
)
@@ -65,19 +68,13 @@ class UrlMappingsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.controller == 'urlMappingsTest'
json.action == 'index'
-
- cleanup:
- client.close()
}
// ========== Path Variable Mappings ==========
def "single path variable is captured"() {
- given:
- def client = createClient()
-
when: "accessing path with variable"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/items/123'),
String
)
@@ -87,17 +84,11 @@ class UrlMappingsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.action == 'show'
json.id == '123'
-
- cleanup:
- client.close()
}
def "path variable accepts alphanumeric values"() {
- given:
- def client = createClient()
-
when: "accessing path with alphanumeric id"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/items/abc-123'),
String
)
@@ -106,17 +97,11 @@ class UrlMappingsSpec extends Specification {
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
json.id == 'abc-123'
-
- cleanup:
- client.close()
}
def "multiple path variables are captured"() {
- given:
- def client = createClient()
-
when: "accessing path with multiple variables"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/archive/2024/03/15'),
String
)
@@ -128,19 +113,13 @@ class UrlMappingsSpec extends Specification {
json.year == '2024'
json.month == '03'
json.day == '15'
-
- cleanup:
- client.close()
}
// ========== Named URL Mappings ==========
def "named mapping routes correctly"() {
- given:
- def client = createClient()
-
when: "accessing named mapping"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/named/test-name'),
String
)
@@ -150,19 +129,13 @@ class UrlMappingsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.action == 'named'
json.name == 'test-name'
-
- cleanup:
- client.close()
}
// ========== Constrained Path Variables ==========
def "constrained path accepts valid values"() {
- given:
- def client = createClient()
-
when: "accessing with valid constrained value"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/codes/ABC'),
String
)
@@ -172,15 +145,9 @@ class UrlMappingsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.action == 'constrained'
json.code == 'ABC'
-
- cleanup:
- client.close()
}
def "constrained path rejects invalid values"() {
- given:
- def client = createClient()
-
when: "accessing with invalid constrained value (lowercase)"
client.toBlocking().exchange(
HttpRequest.GET('/api/codes/abc'),
@@ -188,17 +155,11 @@ class UrlMappingsSpec extends Specification {
)
then: "request is rejected with 404"
- HttpClientResponseException e = thrown()
+ def e = thrown(HttpClientResponseException)
e.status == HttpStatus.NOT_FOUND
-
- cleanup:
- client.close()
}
def "constrained path rejects numeric values"() {
- given:
- def client = createClient()
-
when: "accessing with numeric value"
client.toBlocking().exchange(
HttpRequest.GET('/api/codes/123'),
@@ -206,21 +167,15 @@ class UrlMappingsSpec extends Specification {
)
then: "request is rejected with 404"
- HttpClientResponseException e = thrown()
+ def e = thrown(HttpClientResponseException)
e.status == HttpStatus.NOT_FOUND
-
- cleanup:
- client.close()
}
// ========== HTTP Method Mappings ==========
def "GET request routes to list action"() {
- given:
- def client = createClient()
-
when: "making GET request to resources"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/resources'),
String
)
@@ -229,17 +184,11 @@ class UrlMappingsSpec extends Specification {
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
json.action == 'list'
-
- cleanup:
- client.close()
}
def "POST request routes to save action"() {
- given:
- def client = createClient()
-
when: "making POST request to resources"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.POST('/api/resources', '{}'),
String
)
@@ -249,17 +198,11 @@ class UrlMappingsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.action == 'save'
json.method == 'POST'
-
- cleanup:
- client.close()
}
def "PUT request routes to update action"() {
- given:
- def client = createClient()
-
when: "making PUT request to resources with id"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.PUT('/api/resources/42', '{}'),
String
)
@@ -270,17 +213,11 @@ class UrlMappingsSpec extends Specification {
json.action == 'update'
json.id == '42'
json.method == 'PUT'
-
- cleanup:
- client.close()
}
def "DELETE request routes to delete action"() {
- given:
- def client = createClient()
-
when: "making DELETE request to resources with id"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.DELETE('/api/resources/42'),
String
)
@@ -291,19 +228,13 @@ class UrlMappingsSpec extends Specification {
json.action == 'delete'
json.id == '42'
json.method == 'DELETE'
-
- cleanup:
- client.close()
}
// ========== Optional Path Variables ==========
def "optional path variable with value"() {
- given:
- def client = createClient()
-
when: "accessing with optional variable provided"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/optional/required-value/optional-value'),
String
)
@@ -313,17 +244,11 @@ class UrlMappingsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.required == 'required-value'
json.optional == 'optional-value'
-
- cleanup:
- client.close()
}
def "optional path variable without value uses default"() {
- given:
- def client = createClient()
-
when: "accessing without optional variable"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/optional/required-value'),
String
)
@@ -333,19 +258,13 @@ class UrlMappingsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.required == 'required-value'
json.optional == 'default'
-
- cleanup:
- client.close()
}
// ========== Redirect Mappings ==========
def "redirect mapping performs redirect"() {
- given:
- def client = HttpClient.create(new URL("http://localhost:$serverPort"))
-
when: "accessing redirect mapping"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/old-endpoint').header('Accept', '*/*'),
String
)
@@ -354,19 +273,13 @@ class UrlMappingsSpec extends Specification {
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
json.action == 'index'
-
- cleanup:
- client.close()
}
// ========== Default Controller/Action Mapping ==========
def "default mapping with controller and action"() {
- given:
- def client = createClient()
-
when: "using default mapping pattern"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/urlMappingsTest/show/99'),
String
)
@@ -377,17 +290,11 @@ class UrlMappingsSpec extends Specification {
json.controller == 'urlMappingsTest'
json.action == 'show'
json.id == '99'
-
- cleanup:
- client.close()
}
def "default mapping with format extension"() {
- given:
- def client = createClient()
-
when: "using default mapping with format"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/urlMappingsTest/list.json'),
String
)
@@ -396,19 +303,13 @@ class UrlMappingsSpec extends Specification {
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
json.action == 'list'
-
- cleanup:
- client.close()
}
// ========== Query Parameter Handling ==========
def "query parameters are accessible in action"() {
- given:
- def client = createClient()
-
when: "accessing with query parameters"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/test?param1=value1¶m2=value2'),
String
)
@@ -418,19 +319,13 @@ class UrlMappingsSpec extends Specification {
def json = new JsonSlurper().parseText(response.body())
json.params.param1 == 'value1'
json.params.param2 == 'value2'
-
- cleanup:
- client.close()
}
// ========== HTTP Method Detection ==========
def "request method is correctly detected"() {
- given:
- def client = createClient()
-
when: "making request"
- HttpResponse<String> response = client.toBlocking().exchange(
+ def response = client.toBlocking().exchange(
HttpRequest.GET('/api/method-test'),
String
)
@@ -439,17 +334,11 @@ class UrlMappingsSpec extends Specification {
response.status == HttpStatus.OK
def json = new JsonSlurper().parseText(response.body())
json.method == 'GET'
-
- cleanup:
- client.close()
}
// ========== 404 Not Found ==========
def "non-existent path returns 404"() {
- given:
- def client = createClient()
-
when: "accessing non-existent path"
client.toBlocking().exchange(
HttpRequest.GET('/api/does-not-exist'),
@@ -457,10 +346,7 @@ class UrlMappingsSpec extends Specification {
)
then: "returns 404"
- HttpClientResponseException e = thrown()
+ def e = thrown(HttpClientResponseException)
e.status == HttpStatus.NOT_FOUND
-
- cleanup:
- client.close()
}
}
diff --git a/grails-test-examples/async-events-pubsub-demo/grails-app/services/pubsub/demo/TotalService.groovy b/grails-test-examples/async-events-pubsub-demo/grails-app/services/pubsub/demo/TotalService.groovy
index 350af8c4c4..c3e14020dc 100644
--- a/grails-test-examples/async-events-pubsub-demo/grails-app/services/pubsub/demo/TotalService.groovy
+++ b/grails-test-examples/async-events-pubsub-demo/grails-app/services/pubsub/demo/TotalService.groovy
@@ -16,14 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package pubsub.demo
-import grails.events.Event
-import grails.events.annotation.Subscriber
+import java.util.concurrent.atomic.AtomicInteger
+
import groovy.transform.CompileStatic
-import java.util.concurrent.atomic.AtomicInteger
+import grails.events.Event
+import grails.events.annotation.Subscriber
@CompileStatic
class TotalService {
diff --git a/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/AsyncEventsSpec.groovy b/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/AsyncEventsSpec.groovy
index 6beef73208..9b7966c00a 100644
--- a/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/AsyncEventsSpec.groovy
+++ b/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/AsyncEventsSpec.groovy
@@ -16,15 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package pubsub.demo
-import grails.gorm.transactions.Rollback
-import grails.testing.mixin.integration.Integration
import jakarta.inject.Inject
+
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
+
/**
* Additional integration tests for async events functionality.
*
@@ -50,7 +51,7 @@ class AsyncEventsSpec extends Specification {
totalService.reset()
bookSubscriber.reset()
// Small delay to let any in-flight events complete
- Thread.sleep(100)
+ sleep(100)
}
void "multiple sum events accumulate correctly"() {
@@ -105,12 +106,9 @@ class AsyncEventsSpec extends Specification {
def book3 = bookService.saveBook('Book Three')
then: "all books are saved"
- book1 != null
- book2 != null
- book3 != null
- book1.id != null
- book2.id != null
- book3.id != null
+ book1?.id != null
+ book2?.id != null
+ book3?.id != null
}
void "book events are received after save"() {
diff --git a/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/PubSubSpec.groovy b/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/PubSubSpec.groovy
index abc91ab7cd..4e153641a2 100644
--- a/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/PubSubSpec.groovy
+++ b/grails-test-examples/async-events-pubsub-demo/src/integration-test/groovy/pubsub/demo/PubSubSpec.groovy
@@ -16,15 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package pubsub.demo
-import grails.gorm.transactions.Rollback
-import grails.testing.mixin.integration.Integration
import jakarta.inject.Inject
+
import spock.lang.Specification
import spock.util.concurrent.PollingConditions
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
+
/**
* Created by graemerocher on 03/04/2017.
*/
@@ -42,7 +43,7 @@ class PubSubSpec extends Specification {
totalService.reset()
bookSubscriber.reset()
// Small delay to let any in-flight events from other tests complete
- Thread.sleep(100)
+ sleep(100)
}
void 'Test event bus within Grails'() {
diff --git a/grails-test-examples/async-events-pubsub-demo/src/main/groovy/pubsub/demo/BookSubscriber.groovy b/grails-test-examples/async-events-pubsub-demo/src/main/groovy/pubsub/demo/BookSubscriber.groovy
index 8b8e5c58b7..ed70582941 100644
--- a/grails-test-examples/async-events-pubsub-demo/src/main/groovy/pubsub/demo/BookSubscriber.groovy
+++ b/grails-test-examples/async-events-pubsub-demo/src/main/groovy/pubsub/demo/BookSubscriber.groovy
@@ -16,15 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package pubsub.demo
-import grails.events.annotation.Subscriber
+import java.util.concurrent.ConcurrentLinkedDeque
+
import groovy.transform.CompileStatic
-import org.grails.datastore.mapping.engine.event.PreInsertEvent
+
import org.springframework.stereotype.Component
-import java.util.concurrent.ConcurrentLinkedDeque
+import grails.events.annotation.Subscriber
+import org.grails.datastore.mapping.engine.event.PreInsertEvent
@Component
@CompileStatic
diff --git a/grails-test-examples/gorm/grails-app/domain/gorm/Book.groovy b/grails-test-examples/gorm/grails-app/domain/gorm/Book.groovy
index 7546fd2326..9ee5f18bde 100644
--- a/grails-test-examples/gorm/grails-app/domain/gorm/Book.groovy
+++ b/grails-test-examples/gorm/grails-app/domain/gorm/Book.groovy
@@ -27,8 +27,6 @@ import static grails.gorm.hibernate.mapping.MappingBuilder.*
*/
class Book {
- def testService
-
String title
String isbn
String description
diff --git a/grails-test-examples/gorm/src/integration-test/groovy/gorm/FieldsValidationSpec.groovy b/grails-test-examples/gorm/src/integration-test/groovy/gorm/FieldsValidationSpec.groovy
index 07d1df3cb6..d389dae685 100644
--- a/grails-test-examples/gorm/src/integration-test/groovy/gorm/FieldsValidationSpec.groovy
+++ b/grails-test-examples/gorm/src/integration-test/groovy/gorm/FieldsValidationSpec.groovy
@@ -16,13 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package gorm
+import gorm.pages.AuthorCreatePage
+import gorm.pages.BookCreatePage
+import spock.lang.Stepwise
+
import grails.plugin.geb.ContainerGebSpec
import grails.testing.mixin.integration.Integration
-import gorm.pages.*
-import spock.lang.Stepwise
/**
* Functional tests for fields plugin validation error display in gorm test app.
@@ -30,18 +31,19 @@ import spock.lang.Stepwise
* Tests that validation errors are rendered correctly by the fields plugin
* in scaffolded views.
*/
-@Integration
@Stepwise
+@Integration
class FieldsValidationSpec extends ContainerGebSpec {
+ // TODO: Not fully implemented
def "author email validation shows error for invalid format"() {
when: "navigating to create author page"
- to AuthorCreatePage
+ def page = to AuthorCreatePage
and: "entering invalid email"
- name = 'Test Author'
- email = 'not-an-email'
- createButton.click()
+ page.name = 'Test Author'
+ page.email = 'not-an-email'
+ page.createButton.click()
then: "validation error is displayed"
waitFor {
@@ -55,14 +57,15 @@ class FieldsValidationSpec extends ContainerGebSpec {
}
}
+ // TODO: Not fully implemented
def "author name blank validation shows error"() {
when: "navigating to create author page"
- to AuthorCreatePage
+ def page = to AuthorCreatePage
and: "submitting with blank name"
- name = ''
- email = 'valid@example.com'
- createButton.click()
+ page.name = ''
+ page.email = 'valid@example.com'
+ page.createButton.click()
then: "validation error is displayed for name"
waitFor {
@@ -73,21 +76,22 @@ class FieldsValidationSpec extends ContainerGebSpec {
}
}
+ // TODO: Not fully implemented
def "author unique email constraint shows error"() {
given: "create an author with email"
- to AuthorCreatePage
+ def page = to AuthorCreatePage
// Use a unique email with timestamp to avoid conflicts with other tests
def uniqueEmail = "unique.test.${System.currentTimeMillis()}@example.com"
- name = 'First Author'
- email = uniqueEmail
- createButton.click()
+ page.name = 'First Author'
+ page.email = uniqueEmail
+ page.createButton.click()
waitFor(10) { currentUrl.contains('/author/show/') || currentUrl.contains('/author/index') }
when: "trying to create another author with same email"
- to AuthorCreatePage
- name = 'Second Author'
- email = uniqueEmail
- createButton.click()
+ page = to AuthorCreatePage
+ page.name = 'Second Author'
+ page.email = uniqueEmail
+ page.createButton.click()
then: "unique constraint error is displayed - we should stay on create/save page or see errors"
waitFor(10) {
@@ -105,13 +109,14 @@ class FieldsValidationSpec extends ContainerGebSpec {
}
}
+ // TODO: Not fully implemented
def "book title blank validation shows error"() {
when: "navigating to create book page"
- to BookCreatePage
+ def page = to BookCreatePage
and: "submitting with blank title"
- titleField.value('')
- createButton.click()
+ page.titleField.value('')
+ page.createButton.click()
then: "validation error is displayed"
waitFor {
@@ -123,16 +128,17 @@ class FieldsValidationSpec extends ContainerGebSpec {
}
}
+ // TODO: Not fully implemented
def "book pageCount min validation shows error"() {
when: "navigating to create book page"
- to BookCreatePage
+ def page = to BookCreatePage
and: "entering invalid page count"
- titleField.value('Valid Title')
- if (pageCount.displayed) {
- pageCount.value('0') // min is 1
+ page.titleField.value('Valid Title')
+ if (page.pageCount.displayed) {
+ page.pageCount.value('0') // min is 1
}
- createButton.click()
+ page.createButton.click()
then: "validation error is displayed or book is created (if pageCount nullable)"
waitFor {
@@ -145,16 +151,17 @@ class FieldsValidationSpec extends ContainerGebSpec {
}
}
+ // TODO: Not fully implemented
def "book isbn pattern validation shows error for invalid format"() {
when: "navigating to create book page"
- to BookCreatePage
+ def page = to BookCreatePage
and: "entering invalid ISBN"
- titleField.value('Book With Invalid ISBN')
- if (isbn.displayed) {
- isbn.value('invalid-isbn') // Should match /^(?:\d{10}|\d{13})$/
+ page.titleField.value('Book With Invalid ISBN')
+ if (page.isbn.displayed) {
+ page.isbn.value('invalid-isbn') // Should match /^(?:\d{10}|\d{13})$/
}
- createButton.click()
+ page.createButton.click()
then: "validation error is displayed or book created (isbn nullable)"
waitFor {
@@ -169,29 +176,29 @@ class FieldsValidationSpec extends ContainerGebSpec {
def "validation errors persist field values on re-display"() {
when: "navigating to create author page"
- to AuthorCreatePage
+ def page = to AuthorCreatePage
and: "filling in some fields and triggering validation error"
- name = 'Preserved Name'
- email = 'invalid-email'
- createButton.click()
+ page.name = 'Preserved Name'
+ page.email = 'invalid-email'
+ page.createButton.click()
then: "name field value is preserved"
waitFor { currentUrl.contains('/author/create') || $('input[name=name]').value() }
// The field should retain its value after validation failure
- $('input[name=name]').value() == 'Preserved Name' ||
- $('input[name=name]').value()?.contains('Preserved')
+ $('input[name=name]').value() == 'Preserved Name'
}
+ // TODO: Not fully implemented
def "multiple validation errors displayed together"() {
when: "navigating to create author page"
- to AuthorCreatePage
+ def page = to AuthorCreatePage
and: "submitting with multiple invalid fields"
- name = ''
- email = 'not-valid'
- createButton.click()
+ page.name = ''
+ page.email = 'not-valid'
+ page.createButton.click()
then: "multiple errors should be displayed"
waitFor {
@@ -202,16 +209,17 @@ class FieldsValidationSpec extends ContainerGebSpec {
}
}
+ // TODO: Not fully implemented
def "form retains field values after validation error"() {
when: "navigating to create book page"
- to BookCreatePage
+ def page = to BookCreatePage
and: "filling form with some valid and invalid data"
- titleField.value('') // Invalid - blank
- if (description.displayed) {
- description.value('This is a test description that should be preserved')
+ page.titleField.value('') // Invalid - blank
+ if (page.description.displayed) {
+ page.description.value('This is a test description that should be preserved')
}
- createButton.click()
+ page.createButton.click()
then: "form is re-displayed with preserved values"
waitFor { currentUrl.contains('/book/create') || currentUrl.contains('/book/save') }
@@ -224,14 +232,15 @@ class FieldsValidationSpec extends ContainerGebSpec {
}
}
+ // TODO: Not fully implemented
def "error styling applied to invalid fields"() {
when: "navigating to create author page"
- to AuthorCreatePage
+ def page = to AuthorCreatePage
and: "triggering validation error"
- name.value('')
- email.value('valid@example.com')
- createButton.click()
+ page.name.value('')
+ page.email.value('valid@example.com')
+ page.createButton.click()
then: "we stay on create/save page (validation failed) or error styling is applied"
waitFor {
diff --git a/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormCascadeOperationsSpec.groovy b/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormCascadeOperationsSpec.groovy
index baf5054763..931db50d6e 100644
--- a/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormCascadeOperationsSpec.groovy
+++ b/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormCascadeOperationsSpec.groovy
@@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package gorm
+import spock.lang.Specification
+
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
-import spock.lang.Specification
/**
* Tests for GORM Cascade Operations.
@@ -35,16 +35,16 @@ import spock.lang.Specification
* - Orphan removal
* - Bidirectional associations
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class GormCascadeOperationsSpec extends Specification {
def setup() {
// Clean up test data
- Book.executeUpdate("delete from Book")
- Author.executeUpdate("delete from Author")
- User.executeUpdate("delete from User")
- City.executeUpdate("delete from City")
+ Book.executeUpdate('delete from Book')
+ Author.executeUpdate('delete from Author')
+ User.executeUpdate('delete from User')
+ City.executeUpdate('delete from City')
}
// ============================================
@@ -53,9 +53,9 @@ class GormCascadeOperationsSpec extends Specification {
void "test saving parent cascades to children with addTo"() {
given: "an author with books added via addTo"
- def author = new Author(name: "George Orwell", email: "george@orwell.com")
- author.addToBooks(new Book(title: "1984", pageCount: 328, inStock: true))
- author.addToBooks(new Book(title: "Animal Farm", pageCount: 112, inStock: true))
+ def author = new Author(name: 'George Orwell', email: 'george@orwell.com')
+ author.addToBooks(new Book(title: '1984', pageCount: 328, inStock: true))
+ author.addToBooks(new Book(title: 'Animal Farm', pageCount: 112, inStock: true))
when: "saving only the author"
author.save(flush: true)
@@ -63,17 +63,17 @@ class GormCascadeOperationsSpec extends Specification {
then: "books are also saved (cascade)"
Author.count() == 1
Book.count() == 2
- Book.findByTitle("1984").author == author
- Book.findByTitle("Animal Farm").author == author
+ Book.findByTitle('1984').author == author
+ Book.findByTitle('Animal Farm').author == author
}
void "test saving child with belongsTo saves parent reference"() {
given: "an author"
- def author = new Author(name: "J.K. Rowling", email: "jk@rowling.com")
+ def author = new Author(name: 'J.K. Rowling', email: 'jk@rowling.com')
author.save(flush: true)
when: "creating a book with the author using addToBooks"
- def book = new Book(title: "Harry Potter", pageCount: 309, inStock: true)
+ def book = new Book(title: 'Harry Potter', pageCount: 309, inStock: true)
author.addToBooks(book)
author.save(flush: true)
@@ -84,9 +84,9 @@ class GormCascadeOperationsSpec extends Specification {
void "test cascade save with nested new objects"() {
given: "a new author with new books - all unsaved"
- def author = new Author(name: "Brandon Sanderson", email: "brandon@sanderson.com")
- def book1 = new Book(title: "Mistborn", pageCount: 541, inStock: true)
- def book2 = new Book(title: "The Way of Kings", pageCount: 1007, inStock: true)
+ def author = new Author(name: 'Brandon Sanderson', email: 'brandon@sanderson.com')
+ def book1 = new Book(title: 'Mistborn', pageCount: 541, inStock: true)
+ def book2 = new Book(title: 'The Way of Kings', pageCount: 1007, inStock: true)
author.addToBooks(book1)
author.addToBooks(book2)
@@ -106,39 +106,39 @@ class GormCascadeOperationsSpec extends Specification {
void "test updating parent does not affect children unless changed"() {
given: "an author with books"
- def author = new Author(name: "Original Name", email: "original@email.com")
- author.addToBooks(new Book(title: "Book1", pageCount: 100, inStock: true))
+ def author = new Author(name: 'Original Name', email: 'original@email.com')
+ author.addToBooks(new Book(title: 'Book1', pageCount: 100, inStock: true))
author.save(flush: true)
def bookId = author.books[0].id
when: "updating only the author"
- author.name = "Updated Name"
+ author.name = 'Updated Name'
author.save(flush: true)
then: "book remains unchanged"
def book = Book.get(bookId)
- book.title == "Book1"
+ book.title == 'Book1'
}
void "test dirty checking with associations"() {
given: "an author with books"
- def author = new Author(name: "Test Author", email: "test@author.com")
- author.addToBooks(new Book(title: "Test Book", pageCount: 200, inStock: true))
+ def author = new Author(name: 'Test Author', email: 'test@author.com')
+ author.addToBooks(new Book(title: 'Test Book', pageCount: 200, inStock: true))
author.save(flush: true)
when: "modifying a book through the author"
def book = author.books.first()
- book.title = "Modified Title"
+ book.title = 'Modified Title'
author.save(flush: true)
// Clear session and reload
Author.withSession { it.flush(); it.clear() }
- def reloaded = Book.findByTitle("Modified Title")
+ def reloaded = Book.findByTitle('Modified Title')
then: "book changes are persisted"
reloaded != null
- reloaded.title == "Modified Title"
+ reloaded.title == 'Modified Title'
}
// ============================================
@@ -147,28 +147,28 @@ class GormCascadeOperationsSpec extends Specification {
void "test deleting child does not delete parent"() {
given: "an author with multiple books"
- def author = new Author(name: "Multi Book Author", email: "multi@author.com")
- author.addToBooks(new Book(title: "Keep Me", pageCount: 100, inStock: true))
- author.addToBooks(new Book(title: "Delete Me", pageCount: 100, inStock: true))
+ def author = new Author(name: 'Multi Book Author', email: 'multi@author.com')
+ author.addToBooks(new Book(title: 'Keep Me', pageCount: 100, inStock: true))
+ author.addToBooks(new Book(title: 'Delete Me', pageCount: 100, inStock: true))
author.save(flush: true)
def authorId = author.id
when: "deleting one book"
- def bookToDelete = Book.findByTitle("Delete Me")
+ def bookToDelete = Book.findByTitle('Delete Me')
author.removeFromBooks(bookToDelete)
bookToDelete.delete(flush: true)
then: "author still exists with remaining book"
Author.get(authorId) != null
Author.get(authorId).books.size() == 1
- Book.findByTitle("Keep Me") != null
+ Book.findByTitle('Keep Me') != null
}
void "test belongsTo allows orphan removal"() {
given: "an author with a book"
- def author = new Author(name: "Orphan Test", email: "orphan@test.com")
- def book = new Book(title: "Orphan Book", pageCount: 100, inStock: true)
+ def author = new Author(name: 'Orphan Test', email: 'orphan@test.com')
+ def book = new Book(title: 'Orphan Book', pageCount: 100, inStock: true)
author.addToBooks(book)
author.save(flush: true)
@@ -190,9 +190,9 @@ class GormCascadeOperationsSpec extends Specification {
void "test hasMany cascade save for City and Users"() {
given: "a city with users"
- def city = new City(name: "London")
- city.addToUsers(new User(name: "Alice"))
- city.addToUsers(new User(name: "Bob"))
+ def city = new City(name: 'London')
+ city.addToUsers(new User(name: 'Alice'))
+ city.addToUsers(new User(name: 'Bob'))
when: "saving the city"
city.save(flush: true)
@@ -200,24 +200,24 @@ class GormCascadeOperationsSpec extends Specification {
then: "users are also saved"
City.count() == 1
User.count() == 2
- User.findByName("Alice").city == city
+ User.findByName('Alice').city == city
}
void "test removing user from city"() {
given: "a city with users"
- def city = new City(name: "Paris")
- city.addToUsers(new User(name: "Jean"))
- city.addToUsers(new User(name: "Pierre"))
+ def city = new City(name: 'Paris')
+ city.addToUsers(new User(name: 'Jean'))
+ city.addToUsers(new User(name: 'Pierre'))
city.save(flush: true)
when: "removing one user"
- def jean = User.findByName("Jean")
+ def jean = User.findByName('Jean')
city.removeFromUsers(jean)
jean.delete(flush: true)
then: "user is removed"
city.users.size() == 1
- User.findByName("Jean") == null
+ User.findByName('Jean') == null
}
// ============================================
@@ -226,9 +226,9 @@ class GormCascadeOperationsSpec extends Specification {
void "test bidirectional navigation"() {
given: "an author with books"
- def author = new Author(name: "Nav Author", email: "nav@author.com")
- def book1 = new Book(title: "Nav Book 1", pageCount: 100, inStock: true)
- def book2 = new Book(title: "Nav Book 2", pageCount: 100, inStock: true)
+ def author = new Author(name: 'Nav Author', email: 'nav@author.com')
+ def book1 = new Book(title: 'Nav Book 1', pageCount: 100, inStock: true)
+ def book2 = new Book(title: 'Nav Book 2', pageCount: 100, inStock: true)
author.addToBooks(book1)
author.addToBooks(book2)
author.save(flush: true)
@@ -246,9 +246,9 @@ class GormCascadeOperationsSpec extends Specification {
void "test lazy loading of associations"() {
given: "a city with users"
- def city = new City(name: "Berlin")
- city.addToUsers(new User(name: "Hans"))
- city.addToUsers(new User(name: "Klaus"))
+ def city = new City(name: 'Berlin')
+ city.addToUsers(new User(name: 'Hans'))
+ city.addToUsers(new User(name: 'Klaus'))
city.save(flush: true)
def cityId = city.id
@@ -260,7 +260,7 @@ class GormCascadeOperationsSpec extends Specification {
then: "city is loaded"
loadedCity != null
- loadedCity.name == "Berlin"
+ loadedCity.name == 'Berlin'
and: "users can be lazily loaded"
loadedCity.users.size() == 2
@@ -286,7 +286,7 @@ class GormCascadeOperationsSpec extends Specification {
void "test updating multiple children"() {
given: "an author with multiple books"
- def author = new Author(name: "Batch Author", email: "batch@author.com")
+ def author = new Author(name: 'Batch Author', email: 'batch@author.com')
5.times { i ->
author.addToBooks(new Book(title: "Book ${i}", pageCount: 100 + i, inStock: true))
}
@@ -300,7 +300,7 @@ class GormCascadeOperationsSpec extends Specification {
// Clear and reload
Author.withSession { it.flush(); it.clear() }
- def reloaded = Author.findByName("Batch Author")
+ def reloaded = Author.findByName('Batch Author')
then: "all books updated"
reloaded.books.every { !it.inStock }
@@ -312,9 +312,9 @@ class GormCascadeOperationsSpec extends Specification {
void "test addTo creates bidirectional link"() {
given: "an author and a book"
- def author = new Author(name: "AddTo Author", email: "addto@author.com")
+ def author = new Author(name: 'AddTo Author', email: 'addto@author.com')
author.save(flush: true)
- def book = new Book(title: "AddTo Book", pageCount: 100, inStock: true)
+ def book = new Book(title: 'AddTo Book', pageCount: 100, inStock: true)
when: "using addTo"
author.addToBooks(book)
@@ -327,8 +327,8 @@ class GormCascadeOperationsSpec extends Specification {
void "test removeFrom breaks bidirectional link"() {
given: "an author with a book"
- def author = new Author(name: "RemoveFrom Author", email: "remove@author.com")
- def book = new Book(title: "RemoveFrom Book", pageCount: 100, inStock: true)
+ def author = new Author(name: 'RemoveFrom Author', email: 'remove@author.com')
+ def book = new Book(title: 'RemoveFrom Book', pageCount: 100, inStock: true)
author.addToBooks(book)
author.save(flush: true)
@@ -342,16 +342,16 @@ class GormCascadeOperationsSpec extends Specification {
void "test collection operations on hasMany"() {
given: "a city with users"
- def city = new City(name: "Collection City")
- city.addToUsers(new User(name: "User1"))
- city.addToUsers(new User(name: "User2"))
- city.addToUsers(new User(name: "User3"))
+ def city = new City(name: 'Collection City')
+ city.addToUsers(new User(name: 'User1'))
+ city.addToUsers(new User(name: 'User2'))
+ city.addToUsers(new User(name: 'User3'))
city.save(flush: true)
expect: "collection operations work"
city.users.size() == 3
- city.users.find { it.name == "User1" } != null
- city.users.findAll { it.name.startsWith("User") }.size() == 3
- city.users.collect { it.name }.containsAll(["User1", "User2", "User3"])
+ city.users.find { it.name == 'User1' } != null
+ city.users.findAll { it.name.startsWith('User') }.size() == 3
+ city.users.collect { it.name }.containsAll(['User1', 'User2', 'User3'])
}
}
diff --git a/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormCriteriaQueriesSpec.groovy b/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormCriteriaQueriesSpec.groovy
index d41e02b64b..b73c87d977 100644
--- a/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormCriteriaQueriesSpec.groovy
+++ b/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormCriteriaQueriesSpec.groovy
@@ -16,14 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package gorm
+import spock.lang.Specification
+import spock.lang.Unroll
+
import grails.gorm.DetachedCriteria
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
-import spock.lang.Specification
-import spock.lang.Unroll
/**
* Tests for GORM Criteria Queries - both createCriteria() and DetachedCriteria.
@@ -31,34 +31,34 @@ import spock.lang.Unroll
* Criteria queries provide a type-safe, programmatic way to build
* complex queries without writing HQL strings.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class GormCriteriaQueriesSpec extends Specification {
def setup() {
// Clean up and create fresh test data
- Book.executeUpdate("delete from Book")
- Author.executeUpdate("delete from Author")
+ Book.executeUpdate('delete from Book')
+ Author.executeUpdate('delete from Author')
- def kingAuthor = new Author(name: "Stephen King", email: "stephen@king.com").save(flush: true)
- def clancyAuthor = new Author(name: "Tom Clancy", email: "tom@clancy.com").save(flush: true)
- def grishamAuthor = new Author(name: "John Grisham", email: "john@grisham.com").save(flush: true)
+ def kingAuthor = new Author(name: 'Stephen King', email: 'stephen@king.com').save(flush: true)
+ def clancyAuthor = new Author(name: 'Tom Clancy', email: 'tom@clancy.com').save(flush: true)
+ def grishamAuthor = new Author(name: 'John Grisham', email: 'john@grisham.com').save(flush: true)
// Stephen King books
- new Book(title: "The Stand", isbn: "1234567890", pageCount: 1153, price: 19.99, inStock: true, author: kingAuthor).save(flush: true)
- new Book(title: "It", isbn: "0987654321", pageCount: 1138, price: 18.99, inStock: true, author: kingAuthor).save(flush: true)
- new Book(title: "The Shining", isbn: "1122334455", pageCount: 447, price: 14.99, inStock: false, author: kingAuthor).save(flush: true)
+ new Book(title: 'The Stand', isbn: '1234567890', pageCount: 1153, price: 19.99, inStock: true, author: kingAuthor).save(flush: true)
+ new Book(title: 'It', isbn: '0987654321', pageCount: 1138, price: 18.99, inStock: true, author: kingAuthor).save(flush: true)
+ new Book(title: 'The Shining', isbn: '1122334455', pageCount: 447, price: 14.99, inStock: false, author: kingAuthor).save(flush: true)
// Tom Clancy books
- new Book(title: "The Hunt for Red October", isbn: "2233445566", pageCount: 387, price: 16.99, inStock: true, author: clancyAuthor).save(flush: true)
- new Book(title: "Patriot Games", isbn: "3344556677", pageCount: 540, price: 15.99, inStock: false, author: clancyAuthor).save(flush: true)
+ new Book(title: 'The Hunt for Red October', isbn: '2233445566', pageCount: 387, price: 16.99, inStock: true, author: clancyAuthor).save(flush: true)
+ new Book(title: 'Patriot Games', isbn: '3344556677', pageCount: 540, price: 15.99, inStock: false, author: clancyAuthor).save(flush: true)
// John Grisham books
- new Book(title: "The Firm", isbn: "4455667788", pageCount: 421, price: 13.99, inStock: true, author: grishamAuthor).save(flush: true)
- new Book(title: "A Time to Kill", isbn: "5566778899", pageCount: 515, price: 14.99, inStock: true, author: grishamAuthor).save(flush: true)
+ new Book(title: 'The Firm', isbn: '4455667788', pageCount: 421, price: 13.99, inStock: true, author: grishamAuthor).save(flush: true)
+ new Book(title: 'A Time to Kill', isbn: '5566778899', pageCount: 515, price: 14.99, inStock: true, author: grishamAuthor).save(flush: true)
// Book without author
- new Book(title: "Anonymous Work", pageCount: 100, price: 9.99, inStock: true).save(flush: true)
+ new Book(title: 'Anonymous Work', pageCount: 100, price: 9.99, inStock: true).save(flush: true)
}
// ============================================
@@ -83,7 +83,7 @@ class GormCriteriaQueriesSpec extends Specification {
then: "exact match found"
results.size() == 1
- results[0].title == "The Stand"
+ results[0].title == 'The Stand'
}
void "test criteria with like restriction"() {
@@ -94,7 +94,7 @@ class GormCriteriaQueriesSpec extends Specification {
then: "matching books found"
results.size() == 4 // The Stand, The Shining, The Hunt..., The Firm
- results.every { it.title.startsWith("The") }
+ results.every { it.title.startsWith('The') }
}
void "test criteria with ilike (case insensitive)"() {
@@ -148,7 +148,7 @@ class GormCriteriaQueriesSpec extends Specification {
then: "books without ISBN found"
results.size() == 1
- results[0].title == "Anonymous Work"
+ results[0].title == 'Anonymous Work'
}
void "test criteria with isNotNull"() {
@@ -499,7 +499,7 @@ class GormCriteriaQueriesSpec extends Specification {
void "test HQL with named parameters"() {
when: "executing HQL with named params"
def results = Book.executeQuery(
- "from Book b where b.price > :minPrice and b.inStock = :stock",
+ 'from Book b where b.price > :minPrice and b.inStock = :stock',
[minPrice: 15.00, stock: true]
)
@@ -510,19 +510,19 @@ class GormCriteriaQueriesSpec extends Specification {
void "test HQL with positional parameters"() {
when: "executing HQL with named parameter as alternative to positional"
def results = Book.executeQuery(
- "from Book b where b.title like :pattern",
+ 'from Book b where b.title like :pattern',
[pattern: 'The%']
)
then: "results match"
results.size() == 4
- results.every { it.title.startsWith("The") }
+ results.every { it.title.startsWith('The') }
}
void "test HQL with pagination"() {
when: "executing paginated HQL"
def results = Book.executeQuery(
- "from Book b order by b.title",
+ 'from Book b order by b.title',
[max: 3, offset: 0]
)
@@ -533,7 +533,7 @@ class GormCriteriaQueriesSpec extends Specification {
void "test HQL join query"() {
when: "executing HQL join"
def results = Book.executeQuery(
- "select b from Book b join b.author a where a.name = :authorName",
+ 'select b from Book b join b.author a where a.name = :authorName',
[authorName: 'Stephen King']
)
@@ -545,7 +545,7 @@ class GormCriteriaQueriesSpec extends Specification {
void "test HQL aggregate functions"() {
when: "executing HQL aggregates"
def result = Book.executeQuery(
- "select count(b), avg(b.price), max(b.pageCount) from Book b"
+ 'select count(b), avg(b.price), max(b.pageCount) from Book b'
)[0]
then: "aggregates calculated"
@@ -557,7 +557,7 @@ class GormCriteriaQueriesSpec extends Specification {
void "test HQL group by"() {
when: "executing HQL group by"
def results = Book.executeQuery(
- "select a.name, count(b) from Book b join b.author a group by a.name order by count(b) desc"
+ 'select a.name, count(b) from Book b join b.author a group by a.name order by count(b) desc'
)
then: "grouped results"
@@ -569,7 +569,7 @@ class GormCriteriaQueriesSpec extends Specification {
void "test executeUpdate for bulk operations"() {
when: "executing bulk update"
int updated = Book.executeUpdate(
- "update Book b set b.price = b.price * 1.1 where b.inStock = true"
+ 'update Book b set b.price = b.price * 1.1 where b.inStock = true'
)
then: "bulk update applied"
diff --git a/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormDataServicesSpec.groovy b/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormDataServicesSpec.groovy
index 7359e96196..4a6b8c5026 100644
--- a/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormDataServicesSpec.groovy
+++ b/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormDataServicesSpec.groovy
@@ -16,15 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package gorm
-import grails.gorm.transactions.Rollback
-import grails.testing.mixin.integration.Integration
-import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
import spock.lang.Unroll
+import org.springframework.beans.factory.annotation.Autowired
+
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
+
/**
* Tests for GORM Data Services - abstract class-based services with
* automatically generated implementations for CRUD and finder methods.
@@ -32,8 +33,8 @@ import spock.lang.Unroll
* Data Services are a powerful feature of GORM that generates
* service implementations at compile time based on method signatures.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class GormDataServicesSpec extends Specification {
@Autowired
@@ -41,16 +42,16 @@ class GormDataServicesSpec extends Specification {
def setup() {
// Clean up and create fresh test data
- Book.executeUpdate("delete from Book")
- Author.executeUpdate("delete from Author")
+ Book.executeUpdate('delete from Book')
+ Author.executeUpdate('delete from Author')
- def author = new Author(name: "Stephen King", email: "stephen@king.com").save(flush: true)
+ def author = new Author(name: 'Stephen King', email: 'stephen@king.com').save(flush: true)
- new Book(title: "The Stand", isbn: "1234567890", pageCount: 1153, price: 19.99, inStock: true, author: author).save(flush: true)
- new Book(title: "It", isbn: "0987654321", pageCount: 1138, price: 18.99, inStock: true, author: author).save(flush: true)
- new Book(title: "The Shining", isbn: "1122334455", pageCount: 447, price: 14.99, inStock: false, author: author).save(flush: true)
- new Book(title: "Carrie", isbn: "5566778899", pageCount: 199, price: 12.99, inStock: true).save(flush: true)
- new Book(title: "Salem's Lot", pageCount: 439, price: 15.99, inStock: false).save(flush: true)
+ new Book(title: 'The Stand', isbn: '1234567890', pageCount: 1153, price: 19.99, inStock: true, author: author).save(flush: true)
+ new Book(title: 'It', isbn: '0987654321', pageCount: 1138, price: 18.99, inStock: true, author: author).save(flush: true)
+ new Book(title: 'The Shining', isbn: '1122334455', pageCount: 447, price: 14.99, inStock: false, author: author).save(flush: true)
+ new Book(title: 'Carrie', isbn: '5566778899', pageCount: 199, price: 12.99, inStock: true).save(flush: true)
+ new Book(title: 'Salem\'s Lot', pageCount: 439, price: 15.99, inStock: false).save(flush: true)
}
// ============================================
@@ -59,15 +60,15 @@ class GormDataServicesSpec extends Specification {
void "test get by id"() {
given: "an existing book"
- def book = Book.findByTitle("The Stand")
+ def book = Book.findByTitle('The Stand')
when: "retrieving by id through data service"
def result = bookDataService.get(book.id)
then: "the book is found"
result != null
- result.title == "The Stand"
- result.isbn == "1234567890"
+ result.title == 'The Stand'
+ result.isbn == '1234567890'
}
void "test list with pagination"() {
@@ -85,20 +86,20 @@ class GormDataServicesSpec extends Specification {
void "test save new book"() {
given: "a new book"
- def book = new Book(title: "Doctor Sleep", pageCount: 531, price: 16.99, inStock: true)
+ def book = new Book(title: 'Doctor Sleep', pageCount: 531, price: 16.99, inStock: true)
when: "saving through data service"
def saved = bookDataService.save(book)
then: "book is persisted"
saved.id != null
- saved.title == "Doctor Sleep"
+ saved.title == 'Doctor Sleep'
bookDataService.count() == 6
}
void "test delete"() {
given: "an existing book"
- def book = Book.findByTitle("Carrie")
+ def book = Book.findByTitle('Carrie')
def bookId = book.id
when: "deleting through data service"
@@ -107,7 +108,7 @@ class GormDataServicesSpec extends Specification {
then: "book is removed"
Book.get(bookId) == null
- bookDataService.findByTitle("Carrie") == null
+ bookDataService.findByTitle('Carrie') == null
}
// ============================================
@@ -116,18 +117,18 @@ class GormDataServicesSpec extends Specification {
void "test findByTitle"() {
expect: "finding by title works"
- bookDataService.findByTitle("It") != null
- bookDataService.findByTitle("It").isbn == "0987654321"
- bookDataService.findByTitle("NonExistent") == null
+ bookDataService.findByTitle('It') != null
+ bookDataService.findByTitle('It').isbn == '0987654321'
+ bookDataService.findByTitle('NonExistent') == null
}
void "test findAllByTitle"() {
when: "finding all by title"
- def results = bookDataService.findAllByTitle("The Stand")
+ def results = bookDataService.findAllByTitle('The Stand')
then: "returns matching books"
results.size() == 1
- results[0].title == "The Stand"
+ results[0].title == 'The Stand'
}
void "test findAllByInStock"() {
@@ -166,29 +167,29 @@ class GormDataServicesSpec extends Specification {
void "test existsByTitle (manual implementation)"() {
expect: "exists queries work via manual implementation"
- bookDataService.existsByTitle("It") == true
- bookDataService.existsByTitle("NonExistent") == false
+ bookDataService.existsByTitle('It') == true
+ bookDataService.existsByTitle('NonExistent') == false
}
void "test findByIsbnValue for nullable field"() {
expect: "finding by nullable isbn works via manual HQL"
- bookDataService.findByIsbnValue("1234567890") != null
- bookDataService.findByIsbnValue("1234567890").title == "The Stand"
- bookDataService.findByIsbnValue("0000000000") == null
+ bookDataService.findByIsbnValue('1234567890') != null
+ bookDataService.findByIsbnValue('1234567890').title == 'The Stand'
+ bookDataService.findByIsbnValue('0000000000') == null
bookDataService.findByIsbnValue(null) == null
}
void "test countByTitleLike"() {
expect: "count with like pattern works"
- bookDataService.countByTitleLike("The%") == 2 // "The Stand", "The Shining"
- bookDataService.countByTitleLike("%Lot") == 1 // "Salem's Lot"
+ bookDataService.countByTitleLike('The%') == 2 // 'The Stand', 'The Shining'
+ bookDataService.countByTitleLike('%Lot') == 1 // "Salem's Lot"
}
void "test findByTitleAndOptionalIsbn"() {
expect: "finding with optional isbn works"
- bookDataService.findByTitleAndOptionalIsbn("The Stand", "1234567890") != null
- bookDataService.findByTitleAndOptionalIsbn("The Stand", "wrong-isbn") == null
- bookDataService.findByTitleAndOptionalIsbn("The Stand", null)?.title == "The Stand"
+ bookDataService.findByTitleAndOptionalIsbn('The Stand', '1234567890') != null
+ bookDataService.findByTitleAndOptionalIsbn('The Stand', 'wrong-isbn') == null
+ bookDataService.findByTitleAndOptionalIsbn('The Stand', null)?.title == 'The Stand'
}
// ============================================
@@ -197,11 +198,11 @@ class GormDataServicesSpec extends Specification {
void "test findByTitlePattern with regex-like pattern"() {
when: "searching with pattern"
- def results = bookDataService.findByTitlePattern("The%")
+ def results = bookDataService.findByTitlePattern('The%')
then: "matching books are found"
results.size() == 2
- results*.title.every { it.startsWith("The") }
+ results*.title.every { it.startsWith('The') }
}
void "test findByPageRange"() {
@@ -219,7 +220,7 @@ class GormDataServicesSpec extends Specification {
then: "matching books are found"
results.size() == 1 // Only Carrie at 12.99 is in stock and under 15
- results[0].title == "Carrie"
+ results[0].title == 'Carrie'
}
// ============================================
@@ -228,13 +229,13 @@ class GormDataServicesSpec extends Specification {
void "test searchByTitleHql"() {
when: "searching with HQL pattern"
- def results = bookDataService.searchByTitleHql("The%")
+ def results = bookDataService.searchByTitleHql('The%')
then: "results are found and ordered"
results.size() == 2
// Ordered by title
- results[0].title == "The Shining"
- results[1].title == "The Stand"
+ results[0].title == 'The Shining'
+ results[1].title == 'The Stand'
}
void "test countBooksWithAuthor"() {
@@ -248,7 +249,7 @@ class GormDataServicesSpec extends Specification {
then: "all titles returned"
titles.size() == 5
- titles.containsAll(["The Stand", "It", "The Shining", "Carrie", "Salem's Lot"])
+ titles.containsAll(['The Stand', 'It', 'The Shining', 'Carrie', 'Salem\'s Lot'])
}
// ============================================
@@ -257,13 +258,13 @@ class GormDataServicesSpec extends Specification {
void "test updateStockStatus"() {
given: "an out of stock book"
- def book = Book.findByTitle("The Shining")
+ def book = Book.findByTitle('The Shining')
assert book.inStock == false
when: "updating stock status"
def updated = bookDataService.updateStockStatus(book.id, true)
Book.withSession { it.flush(); it.clear() }
- book = Book.findByTitle("The Shining")
+ book = Book.findByTitle('The Shining')
then: "update is applied"
updated == 1
@@ -276,11 +277,11 @@ class GormDataServicesSpec extends Specification {
void "test deleteByTitle"() {
when: "deleting by title"
- def deleted = bookDataService.deleteByTitle("Carrie")
+ def deleted = bookDataService.deleteByTitle('Carrie')
then: "book is deleted"
deleted == 1
- Book.findByTitle("Carrie") == null
+ Book.findByTitle('Carrie') == null
bookDataService.count() == 4
}
@@ -329,9 +330,9 @@ class GormDataServicesSpec extends Specification {
where:
title | expectedFound
- "The Stand" | true
- "It" | true
- "" | false
- "NONEXISTENT" | false
+ 'The Stand' | true
+ 'It' | true
+ '' | false
+ 'NONEXISTENT' | false
}
}
diff --git a/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormEventsSpec.groovy b/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormEventsSpec.groovy
index c28c8f071b..2f9dd463bb 100644
--- a/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormEventsSpec.groovy
+++ b/grails-test-examples/gorm/src/integration-test/groovy/gorm/GormEventsSpec.groovy
@@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package gorm
+import spock.lang.Specification
+
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
-import spock.lang.Specification
/**
* Tests for GORM Event Listeners.
@@ -37,12 +37,12 @@ import spock.lang.Specification
* Note: These tests focus on entity state changes rather than static event logs
* to avoid test isolation issues with parallel test execution.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class GormEventsSpec extends Specification {
def setup() {
- AuditedEntity.executeUpdate("delete from AuditedEntity")
+ AuditedEntity.executeUpdate('delete from AuditedEntity')
}
// ============================================
@@ -51,24 +51,24 @@ class GormEventsSpec extends Specification {
void "test beforeValidate is called during validation"() {
given: "an entity with untrimmed name"
- def entity = new AuditedEntity(name: " Test Entity ")
+ def entity = new AuditedEntity(name: ' Test Entity ')
when: "validating the entity"
entity.validate()
then: "beforeValidate was called and name was trimmed"
entity.wasValidated == true
- entity.name == "Test Entity"
+ entity.name == 'Test Entity'
}
void "test beforeValidate is called before save validation"() {
when: "saving an entity"
- def entity = new AuditedEntity(name: " BeforeValidate Test ")
+ def entity = new AuditedEntity(name: ' BeforeValidate Test ')
entity.save(flush: true)
then: "beforeValidate was called"
entity.wasValidated == true
- entity.name == "BeforeValidate Test" // Trimmed
+ entity.name == 'BeforeValidate Test' // Trimmed
}
// ============================================
@@ -77,14 +77,14 @@ class GormEventsSpec extends Specification {
void "test beforeInsert sets audit fields"() {
when: "saving a new entity"
- def entity = new AuditedEntity(name: "Insert Test")
+ def entity = new AuditedEntity(name: 'Insert Test')
entity.save(flush: true)
then: "beforeInsert set the audit fields"
entity.dateCreated != null
entity.lastUpdated != null
- entity.createdBy == "test-user"
- entity.updatedBy == "test-user"
+ entity.createdBy == 'test-user'
+ entity.updatedBy == 'test-user'
}
void "test beforeInsert and afterInsert event order"() {
@@ -95,13 +95,13 @@ class GormEventsSpec extends Specification {
then: "entity was persisted with ID (proving events fired)"
entity.id != null
entity.dateCreated != null
- entity.createdBy == "test-user"
+ entity.createdBy == 'test-user'
entity.wasValidated == true
}
void "test afterInsert is called after entity is persisted"() {
when: "saving a new entity"
- def entity = new AuditedEntity(name: "AfterInsert")
+ def entity = new AuditedEntity(name: 'AfterInsert')
entity.save(flush: true)
then: "entity was persisted"
@@ -115,13 +115,13 @@ class GormEventsSpec extends Specification {
void "test beforeUpdate sets lastUpdated"() {
given: "an existing entity"
- def entity = new AuditedEntity(name: "Update Test")
+ def entity = new AuditedEntity(name: 'Update Test')
entity.save(flush: true)
def originalUpdated = entity.lastUpdated
when: "updating the entity"
sleep(10) // Ensure time difference
- entity.description = "Updated description"
+ entity.description = 'Updated description'
entity.save(flush: true)
then: "beforeUpdate updated lastUpdated"
@@ -131,35 +131,35 @@ class GormEventsSpec extends Specification {
void "test beforeUpdate and afterUpdate event order"() {
given: "an existing entity"
- def entity = new AuditedEntity(name: "UpdateOrder")
+ def entity = new AuditedEntity(name: 'UpdateOrder')
entity.save(flush: true)
def originalUpdated = entity.lastUpdated
when: "updating the entity"
sleep(10)
- entity.description = "Changed"
+ entity.description = 'Changed'
entity.save(flush: true)
then: "events fired - lastUpdated was modified by beforeUpdate"
entity.lastUpdated > originalUpdated
entity.updateCount == 1
- entity.updatedBy == "test-user"
+ entity.updatedBy == 'test-user'
}
void "test beforeUpdate can cancel update by returning false"() {
given: "an entity marked as DELETED"
- def entity = new AuditedEntity(name: "Deleted Entity", status: "DELETED")
+ def entity = new AuditedEntity(name: 'Deleted Entity', status: 'DELETED')
entity.save(flush: true)
def originalDescription = entity.description
def originalUpdateCount = entity.updateCount
when: "attempting to update DELETED entity"
- entity.description = "Trying to change"
+ entity.description = 'Trying to change'
entity.save(flush: true)
// Clear session to reload
AuditedEntity.withSession { it.flush(); it.clear() }
- def reloaded = AuditedEntity.findByName("Deleted Entity")
+ def reloaded = AuditedEntity.findByName('Deleted Entity')
then: "update was cancelled - description unchanged"
reloaded.description == originalDescription
@@ -168,18 +168,18 @@ class GormEventsSpec extends Specification {
void "test updateCount increments on each update"() {
given: "a new entity"
- def entity = new AuditedEntity(name: "MultiUpdate")
+ def entity = new AuditedEntity(name: 'MultiUpdate')
entity.save(flush: true)
assert entity.updateCount == 0
when: "updating multiple times"
- entity.description = "First update"
+ entity.description = 'First update'
entity.save(flush: true)
- entity.description = "Second update"
+ entity.description = 'Second update'
entity.save(flush: true)
- entity.description = "Third update"
+ entity.description = 'Third update'
entity.save(flush: true)
then: "updateCount tracks all updates"
@@ -192,7 +192,7 @@ class GormEventsSpec extends Specification {
void "test beforeDelete is called before deletion"() {
given: "an existing entity"
- def entity = new AuditedEntity(name: "Delete Test")
+ def entity = new AuditedEntity(name: 'Delete Test')
entity.save(flush: true)
def entityId = entity.id
@@ -205,7 +205,7 @@ class GormEventsSpec extends Specification {
void "test afterDelete is called after deletion"() {
given: "an existing entity"
- def entity = new AuditedEntity(name: "AfterDelete")
+ def entity = new AuditedEntity(name: 'AfterDelete')
entity.save(flush: true)
def entityId = entity.id
@@ -218,7 +218,7 @@ class GormEventsSpec extends Specification {
void "test delete removes entity from database"() {
given: "an existing entity"
- def entity = new AuditedEntity(name: "DeleteOrder")
+ def entity = new AuditedEntity(name: 'DeleteOrder')
entity.save(flush: true)
def entityId = entity.id
@@ -227,7 +227,7 @@ class GormEventsSpec extends Specification {
then: "entity is gone"
AuditedEntity.get(entityId) == null
- AuditedEntity.findByName("DeleteOrder") == null
+ AuditedEntity.findByName('DeleteOrder') == null
}
// ============================================
@@ -236,7 +236,7 @@ class GormEventsSpec extends Specification {
void "test entity can be loaded from database"() {
given: "a persisted entity"
- def entity = new AuditedEntity(name: "Load Test")
+ def entity = new AuditedEntity(name: 'Load Test')
entity.save(flush: true)
def entityId = entity.id
@@ -248,15 +248,15 @@ class GormEventsSpec extends Specification {
then: "entity is loaded correctly"
loaded != null
- loaded.name == "Load Test"
+ loaded.name == 'Load Test'
loaded.id == entityId
}
void "test list loads multiple entities from database"() {
given: "multiple persisted entities"
- new AuditedEntity(name: "Load1").save(flush: true)
- new AuditedEntity(name: "Load2").save(flush: true)
- new AuditedEntity(name: "Load3").save(flush: true)
+ new AuditedEntity(name: 'Load1').save(flush: true)
+ new AuditedEntity(name: 'Load2').save(flush: true)
+ new AuditedEntity(name: 'Load3').save(flush: true)
// Clear session to force reload
AuditedEntity.withSession { it.flush(); it.clear() }
@@ -266,7 +266,7 @@ class GormEventsSpec extends Specification {
then: "all entities are loaded"
entities.size() == 3
- entities*.name.containsAll(["Load1", "Load2", "Load3"])
+ entities*.name.containsAll(['Load1', 'Load2', 'Load3'])
}
// ============================================
@@ -275,19 +275,19 @@ class GormEventsSpec extends Specification {
void "test full entity lifecycle"() {
when: "creating an entity"
- def entity = new AuditedEntity(name: "Lifecycle")
+ def entity = new AuditedEntity(name: 'Lifecycle')
entity.save(flush: true)
then: "insert lifecycle completed"
entity.id != null
entity.dateCreated != null
- entity.createdBy == "test-user"
+ entity.createdBy == 'test-user'
entity.wasValidated == true
when: "updating the entity"
def originalUpdated = entity.lastUpdated
sleep(10)
- entity.description = "Updated"
+ entity.description = 'Updated'
entity.save(flush: true)
then: "update lifecycle completed"
@@ -304,7 +304,7 @@ class GormEventsSpec extends Specification {
void "test event firing does not occur for failed validation"() {
given: "an invalid entity (blank name)"
- def entity = new AuditedEntity(name: "")
+ def entity = new AuditedEntity(name: '')
when: "attempting to save"
def saved = entity.save()
@@ -316,15 +316,15 @@ class GormEventsSpec extends Specification {
void "test multiple entities can be saved"() {
when: "saving multiple entities"
- def e1 = new AuditedEntity(name: "Entity1").save(flush: true)
- def e2 = new AuditedEntity(name: "Entity2").save(flush: true)
- def e3 = new AuditedEntity(name: "Entity3").save(flush: true)
+ def e1 = new AuditedEntity(name: 'Entity1').save(flush: true)
+ def e2 = new AuditedEntity(name: 'Entity2').save(flush: true)
+ def e3 = new AuditedEntity(name: 'Entity3').save(flush: true)
then: "all entities saved with audit fields"
e1.id != null
e2.id != null
e3.id != null
- [e1, e2, e3].every { it.dateCreated != null && it.createdBy == "test-user" }
+ [e1, e2, e3].every { it.dateCreated != null && it.createdBy == 'test-user' }
}
// ============================================
@@ -333,26 +333,26 @@ class GormEventsSpec extends Specification {
void "test audit fields are automatically populated on insert"() {
when: "saving a new entity"
- def entity = new AuditedEntity(name: "Audit Test")
+ def entity = new AuditedEntity(name: 'Audit Test')
entity.save(flush: true)
then: "all audit fields are set"
entity.dateCreated != null
entity.lastUpdated != null
- entity.createdBy == "test-user"
- entity.updatedBy == "test-user"
+ entity.createdBy == 'test-user'
+ entity.updatedBy == 'test-user'
}
void "test audit fields are updated on update"() {
given: "an existing entity"
- def entity = new AuditedEntity(name: "Audit Update")
+ def entity = new AuditedEntity(name: 'Audit Update')
entity.save(flush: true)
def originalCreated = entity.dateCreated
def originalUpdated = entity.lastUpdated
when: "updating the entity"
sleep(10)
- entity.description = "Changed"
+ entity.description = 'Changed'
entity.save(flush: true)
then: "dateCreated unchanged, lastUpdated modified"
@@ -362,16 +362,16 @@ class GormEventsSpec extends Specification {
void "test name trimming in beforeValidate"() {
when: "saving entities with various whitespace"
- def e1 = new AuditedEntity(name: " leading spaces")
- def e2 = new AuditedEntity(name: "trailing spaces ")
- def e3 = new AuditedEntity(name: " both sides ")
+ def e1 = new AuditedEntity(name: ' leading spaces')
+ def e2 = new AuditedEntity(name: 'trailing spaces ')
+ def e3 = new AuditedEntity(name: ' both sides ')
e1.save(flush: true)
e2.save(flush: true)
e3.save(flush: true)
then: "all names are trimmed"
- e1.name == "leading spaces"
- e2.name == "trailing spaces"
- e3.name == "both sides"
+ e1.name == 'leading spaces'
+ e2.name == 'trailing spaces'
+ e3.name == 'both sides'
}
}
diff --git a/grails-test-examples/gorm/src/integration-test/groovy/gorm/ScaffoldingFunctionalSpec.groovy b/grails-test-examples/gorm/src/integration-test/groovy/gorm/ScaffoldingFunctionalSpec.groovy
index dbead79c9b..3ce0108b50 100644
--- a/grails-test-examples/gorm/src/integration-test/groovy/gorm/ScaffoldingFunctionalSpec.groovy
+++ b/grails-test-examples/gorm/src/integration-test/groovy/gorm/ScaffoldingFunctionalSpec.groovy
@@ -16,26 +16,32 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package gorm
+import gorm.pages.AuthorCreatePage
+import gorm.pages.AuthorListPage
+import gorm.pages.AuthorShowPage
+import gorm.pages.BookCreatePage
+import gorm.pages.BookListPage
+import gorm.pages.BookShowPage
+import spock.lang.Shared
+import spock.lang.Stepwise
+
import grails.plugin.geb.ContainerGebSpec
import grails.testing.mixin.integration.Integration
-import gorm.pages.*
-import spock.lang.Stepwise
/**
* Functional tests for Author and Book scaffolding in the gorm test app.
*
* Tests CRUD operations and relationship handling between Author and Book entities.
*/
-@Integration
@Stepwise
+@Integration
class ScaffoldingFunctionalSpec extends ContainerGebSpec {
// Store created entity IDs for use across @Stepwise tests
- @spock.lang.Shared String authorId
- @spock.lang.Shared String bookId
+ @Shared String authorId
+ @Shared String bookId
def "author list page displays"() {
when: "navigating to author list"
@@ -47,14 +53,14 @@ class ScaffoldingFunctionalSpec extends ContainerGebSpec {
def "can create new author"() {
when: "navigating to create author page"
- to AuthorCreatePage
+ def page = to AuthorCreatePage
and: "filling in author details"
- name = 'Jane Austen'
- email = 'jane.austen@example.com'
+ page.name = 'Jane Austen'
+ page.email = 'jane.austen@example.com'
and: "submitting the form"
- createButton.click()
+ page.createButton.click()
then: "author is created and show page displayed"
waitFor {
@@ -107,10 +113,10 @@ class ScaffoldingFunctionalSpec extends ContainerGebSpec {
def "can create new book with author association"() {
when: "navigating to create book page"
- to BookCreatePage
+ def page = to BookCreatePage
and: "filling in book details"
- titleField = 'Pride and Prejudice'
+ page.titleField = 'Pride and Prejudice'
// Select author if dropdown exists
if ($('select[name="author.id"]').displayed || $('select[name=author]').displayed) {
@@ -121,7 +127,7 @@ class ScaffoldingFunctionalSpec extends ContainerGebSpec {
}
and: "submitting the form"
- createButton.click()
+ page.createButton.click()
then: "book is created"
waitFor {
@@ -166,12 +172,12 @@ class ScaffoldingFunctionalSpec extends ContainerGebSpec {
def "validation errors displayed for invalid author"() {
when: "navigating to create author page"
- to AuthorCreatePage
+ def page = to AuthorCreatePage
and: "submitting with empty required fields"
- name = ''
- email = 'invalid-email'
- createButton.click()
+ page.name = ''
+ page.email = 'invalid-email'
+ page.createButton.click()
then: "validation errors are displayed"
waitFor {
@@ -186,11 +192,11 @@ class ScaffoldingFunctionalSpec extends ContainerGebSpec {
def "validation errors displayed for invalid book"() {
when: "navigating to create book page"
- to BookCreatePage
+ def page = to BookCreatePage
and: "submitting with empty required fields"
- titleField = ''
- createButton.click()
+ page.titleField = ''
+ page.createButton.click()
then: "validation errors are displayed"
waitFor {
@@ -205,9 +211,9 @@ class ScaffoldingFunctionalSpec extends ContainerGebSpec {
def "can delete book"() {
given: "create a book to delete"
- to BookCreatePage
- titleField = 'Book To Delete'
- createButton.click()
+ def page = to BookCreatePage
+ page.titleField = 'Book To Delete'
+ page.createButton.click()
waitFor { currentUrl.contains('/book/show/') }
when: "clicking delete button"
@@ -229,10 +235,10 @@ class ScaffoldingFunctionalSpec extends ContainerGebSpec {
def "can delete author"() {
given: "create an author to delete"
- to AuthorCreatePage
- name = 'Author To Delete'
- email = 'delete.me@example.com'
- createButton.click()
+ def page = to AuthorCreatePage
+ page.name = 'Author To Delete'
+ page.email = 'delete.me@example.com'
+ page.createButton.click()
waitFor { currentUrl.contains('/author/show/') }
when: "clicking delete button"
@@ -262,21 +268,21 @@ class ScaffoldingFunctionalSpec extends ContainerGebSpec {
def "author hasMany books relationship displayed on show page"() {
given: "create author with book"
- to AuthorCreatePage
- name = 'Charles Dickens'
- email = 'charles.dickens@example.com'
- createButton.click()
+ def page = to AuthorCreatePage
+ page.name = 'Charles Dickens'
+ page.email = 'charles.dickens@example.com'
+ page.createButton.click()
waitFor { currentUrl.contains('/author/show/') }
def authorShowUrl = currentUrl
and: "create book for this author"
- to BookCreatePage
- titleField = 'A Tale of Two Cities'
+ page = to BookCreatePage
+ page.titleField = 'A Tale of Two Cities'
def authorSelect = $('select[name="author.id"]') ?: $('select[name=author]')
if (authorSelect?.displayed) {
authorSelect.find('option').last().click()
}
- createButton.click()
+ page.createButton.click()
when: "viewing author show page"
go authorShowUrl
diff --git a/grails-test-examples/gorm/src/integration-test/groovy/gorm/UserSpec.groovy b/grails-test-examples/gorm/src/integration-test/groovy/gorm/UserSpec.groovy
index d132f7265a..43571df20a 100644
--- a/grails-test-examples/gorm/src/integration-test/groovy/gorm/UserSpec.groovy
+++ b/grails-test-examples/gorm/src/integration-test/groovy/gorm/UserSpec.groovy
@@ -16,16 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package gorm
+import spock.lang.Specification
+
+import org.springframework.beans.factory.annotation.Autowired
+
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
-import org.springframework.beans.factory.annotation.Autowired
-import spock.lang.*
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class UserSpec extends Specification {
@Autowired
@@ -34,16 +35,16 @@ class UserSpec extends Specification {
void "Test where query over association id works"() {
given: "Cities and users are set up"
// Create test data within the test to ensure it exists
- def london = City.findByName("London") ?: new City(name: "London")
+ def london = City.findByName('London') ?: new City(name: 'London')
if (!london.id) {
- london.addToUsers(name: "Bob")
- london.addToUsers(name: "Fred")
+ london.addToUsers(name: 'Bob')
+ london.addToUsers(name: 'Fred')
london.save(flush: true)
}
- def paris = City.findByName("Paris") ?: new City(name: "Paris")
+ def paris = City.findByName('Paris') ?: new City(name: 'Paris')
if (!paris.id) {
- paris.addToUsers(name: "Joe")
+ paris.addToUsers(name: 'Joe')
paris.save(flush: true)
}
diff --git a/grails-test-examples/gorm/src/integration-test/groovy/gorm/ValidationConstraintsSpec.groovy b/grails-test-examples/gorm/src/integration-test/groovy/gorm/ValidationConstraintsSpec.groovy
index ff937f23db..fa0562719d 100644
--- a/grails-test-examples/gorm/src/integration-test/groovy/gorm/ValidationConstraintsSpec.groovy
+++ b/grails-test-examples/gorm/src/integration-test/groovy/gorm/ValidationConstraintsSpec.groovy
@@ -16,14 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package gorm
-import grails.gorm.transactions.Rollback
-import grails.testing.mixin.integration.Integration
import spock.lang.Specification
import spock.lang.Unroll
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
+import grails.validation.ValidationException
+
/**
* Comprehensive tests for GORM validation constraints.
*
@@ -39,8 +40,8 @@ import spock.lang.Unroll
* - range: Tests numeric/date range constraints
* - custom validators: Tests custom validation logic
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class ValidationConstraintsSpec extends Specification {
// ============================================
@@ -145,11 +146,11 @@ class ValidationConstraintsSpec extends Specification {
isValid == expectedValid
where:
- name || expectedValid
- 'A' || true // Min length 1
- 'John Doe' || true // Normal length
- 'A' * 100 || true // Max length 100
- 'A' * 101 || false // Over max length
+ name || expectedValid
+ 'A' || true // Min length 1
+ 'John Doe' || true // Normal length
+ 'A' * 100 || true // Max length 100
+ 'A' * 101 || false // Over max length
}
// ============================================
@@ -174,14 +175,14 @@ class ValidationConstraintsSpec extends Specification {
}
where:
- email || expectedValid
- 'valid@example.com' || true
- 'user.name@domain.org' || true
- 'user+tag@domain.com' || true
- 'invalid-email' || false
- 'missing@domain' || false // Missing TLD might fail
- '@nodomain.com' || false
- '' || false // Blank also fails
+ email || expectedValid
+ 'valid@example.com' || true
+ 'user.name@domain.org' || true
+ 'user+tag@domain.com' || true
+ 'invalid-email' || false
+ 'missing@domain' || false // Missing TLD might fail
+ '@nodomain.com' || false
+ '' || false // Blank also fails
}
// ============================================
@@ -256,14 +257,14 @@ class ValidationConstraintsSpec extends Specification {
}
where:
- isbn || expectedValid
- null || true // nullable: true
- '1234567890' || true // 10 digits
- '1234567890123' || true // 13 digits
- '123456789' || false // 9 digits - too short
- '12345678901234' || false // 14 digits - too long
- '123-456-789' || false // Has dashes
- 'ABCDEFGHIJ' || false // Letters
+ isbn || expectedValid
+ null || true // nullable: true
+ '1234567890' || true // 10 digits
+ '1234567890123' || true // 13 digits
+ '123456789' || false // 9 digits - too short
+ '12345678901234' || false // 14 digits - too long
+ '123-456-789' || false // Has dashes
+ 'ABCDEFGHIJ' || false // Letters
}
// ============================================
@@ -441,7 +442,7 @@ class ValidationConstraintsSpec extends Specification {
author.save(failOnError: true)
then: "exception is thrown"
- thrown(grails.validation.ValidationException)
+ thrown(ValidationException)
}
void "test save with validate false skips validation"() {
diff --git a/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy
index 219d42eeab..f0404a0e88 100644
--- a/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy
+++ b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy
@@ -16,14 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package micronaut
-import grails.testing.mixin.integration.Integration
-import io.micronaut.context.ApplicationContext
+import bean.injection.NamedService
+import io.micronaut.context.ApplicationContext as MicronautApplicationContext
+import spock.lang.Specification
+
import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContext as SpringApplicationContext
import org.springframework.context.ApplicationContextAware
-import spock.lang.Specification
+import org.springframework.context.ConfigurableApplicationContext
+
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for Micronaut context coexistence with Spring/Grails context.
@@ -38,11 +42,11 @@ import spock.lang.Specification
class MicronautContextSpec extends Specification implements ApplicationContextAware {
@Autowired
- io.micronaut.context.ApplicationContext micronautContext
+ MicronautApplicationContext micronautContext
- org.springframework.context.ApplicationContext springContext
+ SpringApplicationContext springContext
- void setApplicationContext(org.springframework.context.ApplicationContext applicationContext) {
+ void setApplicationContext(SpringApplicationContext applicationContext) {
this.springContext = applicationContext
}
@@ -59,7 +63,7 @@ class MicronautContextSpec extends Specification implements ApplicationContextAw
void "micronaut beans can be retrieved from micronaut context"() {
when:
- def beans = micronautContext.getBeansOfType(bean.injection.NamedService)
+ def beans = micronautContext.getBeansOfType(NamedService)
then:
beans != null
@@ -82,7 +86,7 @@ class MicronautContextSpec extends Specification implements ApplicationContextAw
void "both contexts share the same application lifecycle"() {
expect:
- micronautContext.isRunning()
- springContext.isActive()
+ micronautContext.running
+ (springContext as ConfigurableApplicationContext).active
}
}
diff --git a/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy
index 0b8ae0af16..bea5adfece 100644
--- a/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy
+++ b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy
@@ -19,15 +19,15 @@
package micronaut
-import grails.testing.mixin.integration.Integration
-import org.springframework.beans.factory.annotation.Autowired
-import spock.lang.Specification
-
-import bean.injection.NamedService
import bean.injection.PrimaryNamedService
+import bean.injection.QualifiedNamedService
import bean.injection.RegularNamedService
import bean.injection.SpecialNamedService
-import bean.injection.QualifiedNamedService
+import spock.lang.Specification
+
+import org.springframework.beans.factory.annotation.Autowired
+
+import grails.testing.mixin.integration.Integration
/**
* Integration tests for Micronaut bean qualifiers in Grails context.
diff --git a/grails-test-examples/scaffolding-fields/grails-app/init/scaffoldingfields/Application.groovy b/grails-test-examples/scaffolding-fields/grails-app/init/scaffoldingfields/Application.groovy
index b44eb32ee3..827e6e3116 100644
--- a/grails-test-examples/scaffolding-fields/grails-app/init/scaffoldingfields/Application.groovy
+++ b/grails-test-examples/scaffolding-fields/grails-app/init/scaffoldingfields/Application.groovy
@@ -19,9 +19,12 @@
package scaffoldingfields
+import groovy.transform.CompileStatic
+
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
+@CompileStatic
class Application extends GrailsAutoConfiguration {
static void main(String[] args) {
GrailsApp.run(Application)
diff --git a/grails-test-examples/scaffolding-fields/grails-app/init/scaffoldingfields/BootStrap.groovy b/grails-test-examples/scaffolding-fields/grails-app/init/scaffoldingfields/BootStrap.groovy
index 8a4e2fb108..d408d09f43 100644
--- a/grails-test-examples/scaffolding-fields/grails-app/init/scaffoldingfields/BootStrap.groovy
+++ b/grails-test-examples/scaffolding-fields/grails-app/init/scaffoldingfields/BootStrap.groovy
@@ -19,93 +19,96 @@
package scaffoldingfields
+import groovy.time.TimeCategory
+
/**
* Bootstrap data for testing scaffolding and fields functionality.
*/
class BootStrap {
- def init = { servletContext ->
- // Create test departments
- def engineering = new Department(name: 'Engineering', description: 'Software development team').save(failOnError: true)
- def marketing = new Department(name: 'Marketing', description: 'Marketing and sales team').save(failOnError: true)
- def hr = new Department(name: 'Human Resources', description: 'HR and recruitment').save(failOnError: true)
+ def init = {
+
+ use(TimeCategory) {
- // Create test projects
- def projectAlpha = new Project(name: 'Project Alpha', code: 'ALPHA', startDate: new Date(), active: true).save(failOnError: true)
- def projectBeta = new Project(name: 'Project Beta', code: 'BETA', startDate: new Date() - 30, active: true).save(failOnError: true)
- def projectGamma = new Project(name: 'Project Gamma', code: 'GAMMA', startDate: new Date() - 60, active: false).save(failOnError: true)
+ // Create test departments
+ def engineering = new Department(name: 'Engineering', description: 'Software development team').save(failOnError: true)
+ def marketing = new Department(name: 'Marketing', description: 'Marketing and sales team').save(failOnError: true)
+ def hr = new Department(name: 'Human Resources', description: 'HR and recruitment').save(failOnError: true)
- // Create test employees with various field types populated
- new Employee(
- firstName: 'John',
- lastName: 'Doe',
- email: 'john.doe@example.com',
- biography: 'Senior software engineer with 10 years of experience in Java and Groovy development.',
- age: 35,
- salary: new BigDecimal('85000.00'),
- hireDate: new Date() - 365,
- active: true,
- status: EmployeeStatus.ACTIVE,
- priority: 'HIGH',
- department: engineering,
- website: new URL('https://johndoe.dev'),
- preferredLocale: Locale.US,
- preferredCurrency: Currency.getInstance('USD'),
- preferredTimezone: TimeZone.getTimeZone('America/New_York'),
- address: new Address(street: '123 Main St', city: 'New York', postalCode: '10001', country: 'USA')
- ).addToProjects(projectAlpha).addToProjects(projectBeta).save(failOnError: true)
+ // Create test projects
+ def projectAlpha = new Project(name: 'Project Alpha', code: 'ALPHA', startDate: new Date(), active: true).save(failOnError: true)
+ def projectBeta = new Project(name: 'Project Beta', code: 'BETA', startDate: new Date() - 30.days, active: true).save(failOnError: true)
+ def projectGamma = new Project(name: 'Project Gamma', code: 'GAMMA', startDate: new Date() - 60.days, active: false).save(failOnError: true)
- new Employee(
- firstName: 'Jane',
- lastName: 'Smith',
- email: 'jane.smith@example.com',
- biography: 'Marketing specialist focused on digital campaigns.',
- age: 28,
- salary: new BigDecimal('65000.00'),
- hireDate: new Date() - 180,
- active: true,
- status: EmployeeStatus.ACTIVE,
- priority: 'MEDIUM',
- department: marketing,
- preferredLocale: Locale.UK,
- preferredCurrency: Currency.getInstance('GBP'),
- preferredTimezone: TimeZone.getTimeZone('Europe/London'),
- address: new Address(street: '456 High Street', city: 'London', postalCode: 'SW1A 1AA', country: 'UK')
- ).addToProjects(projectBeta).save(failOnError: true)
+ // Create test employees with various field types populated
+ def john = new Employee(
+ firstName: 'John',
+ lastName: 'Doe',
+ email: 'john.doe@example.com',
+ biography: 'Senior software engineer with 10 years of experience in Java and Groovy development.',
+ age: 35,
+ salary: new BigDecimal('85000.00'),
+ hireDate: new Date() - 365.days,
+ active: true,
+ status: EmployeeStatus.ACTIVE,
+ priority: 'HIGH',
+ department: engineering,
+ website: new URL('https://johndoe.dev'),
+ preferredLocale: Locale.US,
+ preferredCurrency: Currency.getInstance('USD'),
+ preferredTimezone: TimeZone.getTimeZone('America/New_York'),
+ address: new Address(street: '123 Main St', city: 'New York', postalCode: '10001', country: 'USA')
+ ).addToProjects(projectAlpha).addToProjects(projectBeta).save(failOnError: true)
- new Employee(
- firstName: 'Bob',
- lastName: 'Wilson',
- email: 'bob.wilson@example.com',
- age: 45,
- salary: new BigDecimal('95000.00'),
- hireDate: new Date() - 730,
- active: false,
- status: EmployeeStatus.ON_LEAVE,
- priority: 'LOW',
- department: hr,
- preferredLocale: Locale.CANADA,
- preferredCurrency: Currency.getInstance('CAD'),
- preferredTimezone: TimeZone.getTimeZone('America/Toronto')
- ).save(failOnError: true)
+ new Employee(
+ firstName: 'Jane',
+ lastName: 'Smith',
+ email: 'jane.smith@example.com',
+ biography: 'Marketing specialist focused on digital campaigns.',
+ age: 28,
+ salary: new BigDecimal('65000.00'),
+ hireDate: new Date() - 180.days,
+ active: true,
+ status: EmployeeStatus.ACTIVE,
+ priority: 'MEDIUM',
+ department: marketing,
+ preferredLocale: Locale.UK,
+ preferredCurrency: Currency.getInstance('GBP'),
+ preferredTimezone: TimeZone.getTimeZone('Europe/London'),
+ address: new Address(street: '456 High Street', city: 'London', postalCode: 'SW1A 1AA', country: 'UK')
+ ).addToProjects(projectBeta).save(failOnError: true)
- // Create additional employees for pagination testing
- (1..25).each { i ->
new Employee(
- firstName: "Test${i}",
- lastName: "User${i}",
- email: "test${i}@example.com",
- age: 20 + (i % 30),
- salary: new BigDecimal(50000 + (i * 1000)),
- hireDate: new Date() - (i * 10),
- active: i % 2 == 0,
- status: EmployeeStatus.values()[i % EmployeeStatus.values().length],
- priority: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'][i % 4],
- department: [engineering, marketing, hr][i % 3]
+ firstName: 'Bob',
+ lastName: 'Wilson',
+ email: 'bob.wilson@example.com',
+ age: 45,
+ salary: new BigDecimal('95000.00'),
+ hireDate: new Date() - 730.days,
+ active: false,
+ status: EmployeeStatus.ON_LEAVE,
+ priority: 'LOW',
+ department: hr,
+ preferredLocale: Locale.CANADA,
+ preferredCurrency: Currency.getInstance('CAD'),
+ preferredTimezone: TimeZone.getTimeZone('America/Toronto')
).save(failOnError: true)
- }
- }
- def destroy = {
+ // Create additional employees for pagination testing
+ (1..25).each { i ->
+ new Employee(
+ firstName: "Test${i}",
+ lastName: "User${i}",
+ email: "test${i}@example.com",
+ age: 20 + (i % 30),
+ salary: new BigDecimal(50000 + (i * 1000)),
+ hireDate: new Date() - (i * 10).days,
+ active: i % 2 == 0,
+ status: EmployeeStatus.values()[i % EmployeeStatus.values().length],
+ priority: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'][i % 4],
+ department: [engineering, marketing, hr][i % 3]
+ ).save(failOnError: true)
+ }
+ }
}
}
diff --git a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/CustomTemplatesSpec.groovy b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/CustomTemplatesSpec.groovy
index ae6a41cdd6..5a6b398866 100644
--- a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/CustomTemplatesSpec.groovy
+++ b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/CustomTemplatesSpec.groovy
@@ -82,13 +82,13 @@ class CustomTemplatesSpec extends ContainerGebSpec {
def "custom email template shows validation error with custom styling"() {
given: "navigating to employee create page"
- to EmployeeCreatePage
+ def page = to EmployeeCreatePage
when: "submitting with invalid email"
- firstNameField.value('John')
- lastNameField.value('Doe')
- emailField.value('invalid-email')
- createButton.click()
+ page.firstNameField.value('John')
+ page.lastNameField.value('Doe')
+ page.emailField.value('invalid-email')
+ page.createButton.click()
then: "custom validation error styling is shown or we stay on form"
waitFor {
@@ -104,18 +104,18 @@ class CustomTemplatesSpec extends ContainerGebSpec {
def "custom biography template shows errors with custom styling"() {
given: "navigating to employee create page"
- to EmployeeCreatePage
+ def page = to EmployeeCreatePage
and: "fill in required fields"
- firstNameField.value('Jane')
- lastNameField.value('Smith')
- emailField.value('jane@example.com')
+ page.firstNameField.value('Jane')
+ page.lastNameField.value('Smith')
+ page.emailField.value('jane@example.com')
when: "entering very long biography exceeding constraints"
// The biography field has maxSize: 5000 constraint
// We need to trigger a validation error another way since textarea doesn't enforce easily
// Instead, test that the custom wrapper renders correctly
- createButton.click()
+ page.createButton.click()
then: "form is submitted (biography is nullable so no error expected)"
// This verifies the template is functional during form submission
diff --git a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/FieldTypesSpec.groovy b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/FieldTypesSpec.groovy
index 21f553bf63..cdff3a66ec 100644
--- a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/FieldTypesSpec.groovy
+++ b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/FieldTypesSpec.groovy
@@ -241,10 +241,10 @@ class FieldTypesSpec extends ContainerGebSpec {
$(selector).displayed || true // Graceful if field is optional
where:
- fieldName | selector
- 'firstName' | 'input[name="firstName"]'
- 'lastName' | 'input[name="lastName"]'
- 'email' | 'input[name="email"]'
- 'active' | 'input[name="active"]'
+ fieldName | selector
+ 'firstName' | 'input[name="firstName"]'
+ 'lastName' | 'input[name="lastName"]'
+ 'email' | 'input[name="email"]'
+ 'active' | 'input[name="active"]'
}
}
diff --git a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/PaginationSpec.groovy b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/PaginationSpec.groovy
index 6efc30c11e..e7fb0d9121 100644
--- a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/PaginationSpec.groovy
+++ b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/PaginationSpec.groovy
@@ -16,15 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package scaffoldingfields
-import grails.plugin.geb.ContainerGebSpec
-import grails.testing.mixin.integration.Integration
-import scaffoldingfields.pages.EmployeeListPage
import scaffoldingfields.pages.DepartmentListPage
+import scaffoldingfields.pages.EmployeeListPage
import spock.lang.Stepwise
+import grails.plugin.geb.ContainerGebSpec
+import grails.testing.mixin.integration.Integration
+
/**
* Functional tests for scaffolded list pagination and sorting.
*
@@ -35,26 +35,23 @@ import spock.lang.Stepwise
* 4. Sort state is preserved across page navigation
* 5. Item count and page info are displayed correctly
*/
-@Integration
@Stepwise
+@Integration
class PaginationSpec extends ContainerGebSpec {
- def setupSpec() {
- // Bootstrap data should create enough records for pagination
- }
-
+ // TODO: Not fully implemented
def "employee list displays pagination when records exceed page size"() {
when: "navigating to employee list"
- to EmployeeListPage
+ def page = to EmployeeListPage
then: "list page is displayed"
at EmployeeListPage
and: "table with data is shown"
- dataTable.displayed
+ page.dataTable.displayed
and: "rows are present"
- tableRows.size() > 0
+ page.tableRows.size() > 0
}
def "employee list shows sortable column headers"() {
@@ -67,54 +64,46 @@ class PaginationSpec extends ContainerGebSpec {
$('th', text: contains('First')).displayed
}
+ // TODO: Not fully implemented
def "clicking column header sorts ascending"() {
given: "on employee list page"
- to EmployeeListPage
+ def page = to EmployeeListPage
when: "clicking on a sortable column header"
- def sortLink = $('th a').find { it.text()?.toLowerCase()?.contains('first') || it.text()?.toLowerCase()?.contains('last') }
- if (sortLink) {
- sortLink.click()
- waitFor { currentUrl.contains('sort=') || tableRows.size() > 0 }
- }
+ page.sortLink('First Name').click()
then: "page reloads with sorted data"
at EmployeeListPage
- tableRows.size() > 0
+ currentUrl.contains('sort=')
+ page.tableRows.size() > 0
}
+ // TODO: Not fully implemented
def "clicking same column header again sorts descending"() {
given: "on employee list page with ascending sort"
- to EmployeeListPage
- def sortLink = $('th a').find { it.text()?.toLowerCase()?.contains('first') || it.text()?.toLowerCase()?.contains('last') }
- if (sortLink) {
- sortLink.click()
- waitFor { tableRows.size() > 0 }
- }
+ def page = to EmployeeListPage
+ page.sortLink('First Name').click()
when: "clicking the same column header again"
- sortLink = $('th a').find { it.text()?.toLowerCase()?.contains('first') || it.text()?.toLowerCase()?.contains('last') }
- if (sortLink) {
- sortLink.click()
- waitFor { tableRows.size() > 0 }
- }
+ page.sortLink('First Name').click()
then: "sort order changes"
at EmployeeListPage
- tableRows.size() > 0
+ page.tableRows.size() > 0
}
+ // TODO: Not fully implemented
def "pagination links navigate between pages"() {
given: "on employee list page"
- to EmployeeListPage
- def initialRowCount = tableRows.size()
+ def page = to EmployeeListPage
+ //def initialRowCount = page.tableRows.size()
when: "checking for pagination controls"
def nextLink = $('a.step', text: contains('Next')) ?: $('a', text: contains('Next')) ?: $('a.nextLink')
def paginationExists = nextLink?.displayed ?: $('nav.pagination').displayed ?: $('ul.pagination').displayed
then: "list is displayed"
- tableRows.size() > 0
+ page.tableRows.size() > 0
// Pagination may or may not be present depending on data volume
// If pagination exists, we test it; otherwise we just verify the list works
@@ -122,37 +111,41 @@ class PaginationSpec extends ContainerGebSpec {
def "department list displays records"() {
when: "navigating to department list"
- to DepartmentListPage
+ def page = to DepartmentListPage
then: "list page is displayed"
at DepartmentListPage
and: "table with data is shown"
- dataTable.displayed
+ page.dataTable.displayed
and: "rows are present from bootstrap data"
- tableRows.size() > 0
+ page.tableRows.size() > 0
}
def "department list shows department names"() {
when: "navigating to department list"
- to DepartmentListPage
+ def page = to DepartmentListPage
then: "department names are visible"
- tableRows.find { it.text()?.contains('Engineering') || it.text()?.contains('Marketing') || it.text()?.contains('Sales') }
+ page.tableRows.find {
+ it.text()?.contains('Engineering') || it.text()?.contains('Marketing') || it.text()?.contains('Sales')
+ }
}
+ // TODO: Not fully implemented
def "list shows correct record count information"() {
when: "navigating to employee list"
- to EmployeeListPage
+ def page = to EmployeeListPage
then: "page displays records"
- tableRows.size() > 0
+ page.tableRows.size() > 0
// Record count info may be displayed in various formats
// Just verify the page is functional
}
+ // TODO: Not fully implemented
def "empty search results show appropriate message"() {
when: "navigating to employee list with filter that matches nothing"
go '/employee/index?firstName=NONEXISTENT_12345'
@@ -163,45 +156,48 @@ class PaginationSpec extends ContainerGebSpec {
// Either shows empty table or no results message
}
+ // TODO: Not fully implemented
def "list maintains state across browser refresh"() {
given: "on sorted employee list"
- to EmployeeListPage
+ def page = to EmployeeListPage
when: "sorting by a column"
def sortLink = $('th a').first()
if (sortLink?.displayed) {
sortLink.click()
- waitFor { tableRows.size() > 0 }
+ waitFor { page.tableRows.size() > 0 }
}
def currentUrlBeforeRefresh = currentUrl
and: "refreshing the page"
driver.navigate().refresh()
- waitFor { tableRows.size() > 0 }
+ waitFor { page.tableRows.size() > 0 }
then: "sort parameters are preserved in URL or list is still functional"
at EmployeeListPage
- tableRows.size() > 0
+ page.tableRows.size() > 0
}
+ // TODO: Not fully implemented
def "pagination works with sorting applied"() {
given: "on employee list page"
- to EmployeeListPage
+ def page = to EmployeeListPage
when: "applying a sort"
def sortLink = $('th a').first()
if (sortLink?.displayed) {
sortLink.click()
- waitFor { tableRows.size() > 0 }
+ waitFor { page.tableRows.size() > 0 }
}
then: "sorted list is displayed"
- tableRows.size() > 0
+ page.tableRows.size() > 0
// If pagination is available with current data set, verify it works with sort
// This ensures sort and pagination don't conflict
}
+ // TODO: Not fully implemented
def "max parameter limits displayed records"() {
when: "navigating with max parameter"
go '/employee/index?max=2'
@@ -212,6 +208,7 @@ class PaginationSpec extends ContainerGebSpec {
// The max parameter should limit results (if there's data)
}
+ // TODO: Not fully implemented
def "offset parameter skips records"() {
when: "navigating with offset parameter"
go '/employee/index?offset=1'
@@ -222,6 +219,7 @@ class PaginationSpec extends ContainerGebSpec {
// The offset parameter should skip initial records
}
+ // TODO: Not fully implemented
def "combined max and offset parameters work together"() {
when: "navigating with both max and offset"
go '/employee/index?max=5&offset=0'
diff --git a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/RelationshipsFunctionalSpec.groovy b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/RelationshipsFunctionalSpec.groovy
index 74f6537635..4f40f4fd73 100644
--- a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/RelationshipsFunctionalSpec.groovy
+++ b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/RelationshipsFunctionalSpec.groovy
@@ -16,9 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package scaffoldingfields
+import spock.lang.PendingFeature
+
import grails.gorm.transactions.Rollback
import grails.plugin.geb.ContainerGebSpec
import grails.testing.mixin.integration.Integration
@@ -27,8 +28,8 @@ import grails.testing.mixin.integration.Integration
* Functional tests for relationship handling in scaffolded views.
* Tests belongsTo, hasMany, and manyToMany relationship rendering.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class RelationshipsFunctionalSpec extends ContainerGebSpec {
// ==================== BELONGSTO RELATIONSHIP ====================
@@ -38,7 +39,7 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
go '/employee/create'
then: "department select is present with options"
- def deptSelect = $('select[name="department"], select[name="department.id"]')
+ def deptSelect = $('select[name="department.id"]')
deptSelect.displayed
and: "select contains departments from bootstrap"
@@ -54,11 +55,8 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
go '/employee/create'
when: "selecting a department and filling required fields"
- def deptSelect = $('select[name="department"], select[name="department.id"]')
- if (deptSelect.displayed) {
- // Select Engineering department
- deptSelect.find('option', text: contains('Engineering')).click()
- }
+ def deptSelect = $('select[name="department.id"]')
+ deptSelect.find('option', text: contains('Engineering')).click()
$('input[name="firstName"]').value('Related')
$('input[name="lastName"]').value('Employee')
$('input[name="email"]').value('related.employee@example.com')
@@ -67,7 +65,8 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
$('input[type="submit"], button[type="submit"]').click()
then: "employee is created successfully"
- !title.contains('Create') || title.contains('Employee')
+ !title.contains('Create')
+ title.contains('Show Employee')
}
def "Show page displays belongsTo association"() {
@@ -76,7 +75,8 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
then: "department is displayed"
def pageContent = $('body').text()
- pageContent.contains('Department') || pageContent.contains('Engineering') || true
+ pageContent.contains('Department')
+ pageContent.contains('Engineering')
}
def "Edit page preserves belongsTo selection"() {
@@ -85,10 +85,8 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
then: "department select has a value selected"
def deptSelect = $('select[name="department"], select[name="department.id"]')
- if (deptSelect.displayed) {
- def selectedOption = deptSelect.find('option:checked, option[selected]')
- selectedOption.size() >= 0 // May or may not have selection
- }
+ def selectedOption = deptSelect.find('option:checked, option[selected]')
+ selectedOption.text().contains('Engineering')
}
// ==================== HASMANY RELATIONSHIP (Parent Side) ====================
@@ -97,12 +95,13 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
when: "navigating to a department show page"
go '/department/show/1'
- then: "employees list may be displayed"
+ then: "employees list is displayed"
def pageContent = $('body').text()
- // Employees might be shown as a list
- pageContent.contains('Employee') || pageContent.contains('John') || true
+ pageContent.contains('Employees')
+ pageContent.contains('John')
}
+ // TODO: Not fully implemented
def "Department list shows employee count or link"() {
when: "navigating to department list"
go '/department/index'
@@ -121,11 +120,11 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
then: "projects field may be multi-select"
def projectsSelect = $('select[name="projects"]')
// HasMany on create might not show or might be multi-select
- !projectsSelect.displayed ||
- projectsSelect.attr('multiple') != null ||
+ projectsSelect.attr('multiple') != null
projectsSelect.find('option').size() >= 0
}
+ // TODO: Not fully implemented
def "Can select multiple hasMany items"() {
given: "navigating to employee edit page with projects"
go '/employee/edit/1'
@@ -134,27 +133,30 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
def projectsSelect = $('select[name="projects"]')
then: "can interact with multi-select if present"
- !projectsSelect.displayed || true // Graceful if not shown
+ projectsSelect.displayed
}
// ==================== MANYTOMANY RELATIONSHIP ====================
+ @PendingFeature(reason = 'Many-to-many relationship does not show up on page')
def "Project shows many-to-many employees"() {
when: "navigating to project show page"
go '/project/show/1'
then: "employees may be displayed"
def pageContent = $('body').text()
- pageContent.contains('Employee') || pageContent.contains('John') || true
+ pageContent.contains('Employees')
+ pageContent.contains('John')
}
def "Project edit allows selecting multiple employees"() {
when: "navigating to project edit page"
go '/project/edit/1'
- then: "employees select may be present"
+ then: "employees is present"
def employeesSelect = $('select[name="employees"]')
- !employeesSelect.displayed || employeesSelect.find('option').size() >= 0
+ employeesSelect.displayed
+ employeesSelect.find('option').size() >= 0
}
// ==================== EMBEDDED RELATIONSHIP ====================
@@ -170,9 +172,10 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
def postalCodeField = $('input[name="address.postalCode"]')
def countryField = $('input[name="address.country"]')
- // At least some embedded fields should be present
- streetField.displayed || cityField.displayed ||
- postalCodeField.displayed || countryField.displayed || true
+ streetField.displayed
+ cityField.displayed
+ postalCodeField.displayed
+ countryField.displayed
}
def "Can save entity with embedded object"() {
@@ -185,34 +188,33 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
$('input[name="email"]').value('embedded.address@example.com')
def streetField = $('input[name="address.street"]')
- if (streetField.displayed) {
- streetField.value('789 Test Street')
- }
+ streetField.value('789 Test Street')
def cityField = $('input[name="address.city"]')
- if (cityField.displayed) {
- cityField.value('Test City')
- }
+ cityField.value('Test City')
and: "submitting the form"
$('input[type="submit"], button[type="submit"]').click()
then: "entity is created"
- !title.contains('Create') || title.contains('Employee')
+ !title.contains('Create')
+ title.contains('Employee')
}
def "Show page displays embedded object properties"() {
when: "navigating to employee show page with address"
go '/employee/show/1'
- then: "address fields may be displayed"
+ then: "address fields are displayed"
def pageContent = $('body').text()
// Check for any address-related content
- pageContent.contains('Address') || pageContent.contains('Street') ||
- pageContent.contains('New York') || true
+ pageContent.contains('Address')
+ pageContent.contains('Street')
+ pageContent.contains('New York')
}
// ==================== RELATIONSHIP CONSISTENCY ====================
+ // TODO: Not fully implemented
def "Creating employee with department updates department employee count"() {
given: "count existing employees in engineering"
go '/department/show/1'
@@ -221,16 +223,15 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
when: "creating new employee in engineering"
go '/employee/create'
def deptSelect = $('select[name="department"], select[name="department.id"]')
- if (deptSelect.displayed) {
- deptSelect.find('option', text: contains('Engineering')).click()
- }
+ deptSelect.find('option', text: contains('Engineering')).click()
$('input[name="firstName"]').value('New')
$('input[name="lastName"]').value('EngineerTest')
$('input[name="email"]').value('new.engineer.test@example.com')
$('input[type="submit"], button[type="submit"]').click()
then: "employee was created"
- !title.contains('Create') || title.contains('Employee')
+ !title.contains('Create')
+ title.contains('Employee')
}
// ==================== NULL ASSOCIATION HANDLING ====================
@@ -249,7 +250,8 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
$('input[type="submit"], button[type="submit"]').click()
then: "entity is created without association"
- !title.contains('Create') || title.contains('Employee')
+ !title.contains('Create')
+ title.contains('Employee')
}
def "Show page handles null associations gracefully"() {
@@ -257,6 +259,8 @@ class RelationshipsFunctionalSpec extends ContainerGebSpec {
go '/employee/show/3' // Bob Wilson has no projects
then: "page renders without errors"
- title.contains('Employee') || title.contains('Show') || $('body').text().contains('Wilson')
+ title.contains('Employee')
+ title.contains('Show')
+ $('body').text().contains('Wilson')
}
}
diff --git a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/ValidationFunctionalSpec.groovy b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/ValidationFunctionalSpec.groovy
index c44f00c366..10cbf2ed3a 100644
--- a/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/ValidationFunctionalSpec.groovy
+++ b/grails-test-examples/scaffolding-fields/src/integration-test/groovy/scaffoldingfields/ValidationFunctionalSpec.groovy
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package scaffoldingfields
import grails.gorm.transactions.Rollback
@@ -27,12 +26,13 @@ import grails.testing.mixin.integration.Integration
* Functional tests for form validation error display.
* Tests that validation errors are properly rendered in scaffolded views.
*/
-@Integration(applicationClass = Application)
@Rollback
+@Integration
class ValidationFunctionalSpec extends ContainerGebSpec {
// ==================== REQUIRED FIELD VALIDATION ====================
+ // TODO: Not fully implemented
def "Blank required field shows error message"() {
given: "navigating to the employee create page"
go '/employee/create'
@@ -52,6 +52,7 @@ class ValidationFunctionalSpec extends ContainerGebSpec {
}
}
+ // TODO: Not fully implemented
def "Blank first name shows specific error"() {
given: "navigating to the employee create page"
go '/employee/create'
@@ -70,7 +71,7 @@ class ValidationFunctionalSpec extends ContainerGebSpec {
}
// ==================== EMAIL VALIDATION ====================
-
+ // TODO: Not fully implemented
def "Invalid email format shows error message"() {
given: "navigating to the employee create page"
go '/employee/create'
@@ -102,16 +103,15 @@ class ValidationFunctionalSpec extends ContainerGebSpec {
then: "error is shown for duplicate email - validation prevents navigation away"
waitFor(5) {
- title == 'Create Employee' ||
- currentUrl.contains('/employee/create') ||
- $('.errors, .alert-danger, .invalid-feedback').displayed ||
- // If unique constraint not configured, may go to show page - still valid test
- currentUrl.contains('/employee/show/')
+ title == 'Create Employee'
+ $('.alert-danger').displayed
+ pageSource.contains('Property [email] of class [class scaffoldingfields.Employee] with value [john.doe@example.com] must be unique')
}
}
// ==================== NUMERIC VALIDATION ====================
+ // TODO: Not fully implemented
def "Age below minimum shows error"() {
given: "navigating to the employee create page"
go '/employee/create'
@@ -121,9 +121,7 @@ class ValidationFunctionalSpec extends ContainerGebSpec {
$('input[name="lastName"]').value('Person')
$('input[name="email"]').value('young.person@example.com')
def ageField = $('input[name="age"]')
- if (ageField.displayed) {
- ageField.value('10')
- }
+ ageField.value('10')
$('input[type="submit"], button[type="submit"]').click()
then: "error is shown if age was provided"
@@ -131,6 +129,7 @@ class ValidationFunctionalSpec extends ContainerGebSpec {
true
}
+ // TODO: Not fully implemented
def "Age above maximum shows error"() {
... 139 lines suppressed ...