Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/publish-root-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def publishedProjects = [
'grails-testing-support-dbcleanup-h2',
'grails-testing-support-dbcleanup-postgresql',
'grails-testing-support-http-client',
'grails-testing-support-latency',
'grails-testing-support-mongodb',
'grails-testing-support-views-gson',
'grails-testing-support-web',
Expand Down
9 changes: 9 additions & 0 deletions grails-doc/src/en/guide/introduction/whatsNew.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ Integration tests that implement `HttpClientSupport` can now use `httpPostForm(.
request bodies. The helper URL-encodes form fields using UTF-8, and repeated values in a `Collection` or array are encoded as
repeated keys in insertion order.

==== Latency Testing Support

The new `grails-testing-support-latency` module injects artificial latency into the application under test, so that
requests randomly take longer. Slower responses widen the window in which timing bugs occur, turning intermittently
flaky functional tests — assertions that race a navigation, missing waits, ordering assumptions — into deterministic
failures. The module is inert until `grails.testing.latency.enabled` is set to `true`, and the delay range, the
fraction of requests affected, and the url patterns are all configurable. See the
xref:testing/functionalTesting[Functional Testing] section for details.

==== GORM for MongoDB: TTL, Text, and Reconciled Indexes

GORM for MongoDB can now declare a https://www.mongodb.com/docs/manual/core/index-ttl/[TTL index] directly in the mapping DSL.
Expand Down
66 changes: 66 additions & 0 deletions grails-doc/src/en/guide/testing/functionalTesting.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,69 @@ class MySpec extends Specification {

}
----


==== Simulating Latency

Functional tests are prone to timing bugs: an assertion that races a page navigation, a missing
wait, or an assumption about response ordering may pass reliably on a fast local machine and fail
intermittently on slower CI infrastructure. The `grails-testing-support-latency` module makes such
bugs reproducible by injecting artificial latency into the application under test, so that requests
randomly take longer.

Add the dependency to your integration test configuration:

[source,groovy]
----
dependencies {
integrationTestRuntimeOnly 'org.apache.grails:grails-testing-support-latency'
}
----

The module is inert until enabled. To activate it, set `grails.testing.latency.enabled` to `true`,
for example in `application.yml` under a test environment, or as a system property on the test JVM:

[source,groovy]
----
tasks.named('integrationTest', Test) {
systemProperty('grails.testing.latency.enabled', 'true')
}
----

When enabled, a servlet filter is registered with the highest precedence that delays each matched
request by a random amount of time before it is processed, mirroring what a slow network or an
overloaded server looks like to the browser or HTTP client driving the test.

The following configuration properties are supported under the `grails.testing.latency` prefix:

[format="csv", options="header"]
|===
Property,Default Value,Description
enabled,`false`,Whether to inject artificial latency
min-delay,`0`,Lower bound (inclusive) of the random delay per request
max-delay,`2s`,Upper bound (inclusive) of the random delay per request
probability,`1.0`,Fraction of requests to delay between `0.0` (none) and `1.0` (all)
url-patterns,`/*`,Servlet url patterns the filter applies to
seed,none,Optional fixed random seed for reproducible delay sequences
|===

Durations without a unit suffix are interpreted as milliseconds; suffixes such as `500ms`, `2s` are
also supported. For example, to delay half of all requests by between a quarter and two seconds:

[source,yaml]
----
grails:
testing:
latency:
enabled: true
min-delay: 250ms
max-delay: 2s
probability: 0.5
----

A useful pattern is a dedicated CI job (or an occasional local run) that executes the functional
test suite with latency enabled and a generous `max-delay`. Tests that only pass on a fast server —
for example a Geb assertion that reads the page before a click-triggered navigation completes —
will fail deterministically under latency instead of flaking rarely in CI.

WARNING: This module is a testing aid. Never enable it in production configuration.
48 changes: 48 additions & 0 deletions grails-test-examples/latency/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

plugins {
id 'org.apache.grails.buildsrc.properties'
id 'org.apache.grails.buildsrc.dependency-validator'
id 'org.apache.grails.buildsrc.compile'
id 'org.apache.grails.buildsrc.vulnerability-scan'
}

version = '0.1'
group = 'latencyapp'

apply plugin: 'org.apache.grails.gradle.grails-web'

dependencies {
implementation platform(project(':grails-bom'))

implementation 'org.apache.grails:grails-dependencies-starter-web'
implementation 'com.h2database:h2'
implementation 'org.apache.grails:grails-data-hibernate5'
runtimeOnly 'com.zaxxer:HikariCP'

integrationTestImplementation 'org.apache.grails.testing:grails-testing-support-core'
integrationTestImplementation project(':grails-testing-support-http-client')
integrationTestRuntimeOnly project(':grails-testing-support-latency')
}

apply {
from rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle')
from rootProject.layout.projectDirectory.file('gradle/grails-extension-gradle-config.gradle')
}
47 changes: 47 additions & 0 deletions grails-test-examples/latency/grails-app/conf/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

---
grails:
profile: web
codegen:
defaultPackage: latencyapp
mime:
types:
json:
- application/json
html:
- text/html
xml:
- text/xml
- application/xml
all: '*/*'

environments:
test:
dataSource:
dbCreate: create-drop
url: jdbc:h2:mem:testDb;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=10000
driverClassName: org.h2.Driver
username: sa
password: ''
grails:
testing:
latency:
enabled: true
min-delay: 1500ms
max-delay: 3s
url-patterns:
- '/slow/*'
31 changes: 31 additions & 0 deletions grails-test-examples/latency/grails-app/conf/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.

-->
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="error">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package latencyapp

import grails.compiler.GrailsCompileStatic

/**
* Outside the configured {@code grails.testing.latency.url-patterns}, so responses
* from this controller are served without artificial delay.
*/
@GrailsCompileStatic
class FastController {

def ping() {
render('pong')
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package latencyapp

import grails.compiler.GrailsCompileStatic

/**
* Matched by the configured {@code grails.testing.latency.url-patterns}, so responses
* from this controller are artificially delayed.
*/
@GrailsCompileStatic
class SlowController {

def ping() {
render('pong')
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package latencyapp

class UrlMappings {

static mappings = {
"/$controller/$action?/$id?(.$format)?" {
constraints {
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package latencyapp

import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration

class Application extends GrailsAutoConfiguration {

static void main(String[] args) {
GrailsApp.run(Application, args)
}

}
Loading
Loading