You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/05/28 21:36:46 UTC
(grails-static-website) 09/09: Add a Testing chapter to grails-docker-bootbuildimage v8
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch add-comprehensive-tests-v8-guides
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git
commit e6573bf5c89ebdedca59bbda78fba185fa8f7eac
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Thu May 28 17:08:36 2026 -0400
Add a Testing chapter to grails-docker-bootbuildimage v8
The app shipped only the forge-default Geb spec and its test build could not
resolve Testcontainers. The sample now runs 4 green functional tests over the
Actuator health surface the guide configures.
New "Testing the Health Probes" chapter (registered in conf/guides.yml toc)
vendoring:
- application-test.yml: testcontainers-bom dependency note + create-drop schema
and databasemigration.updateOnStart false (the database-migration plugin
otherwise blocks schema creation in the test env)
- ActuatorHealthFunctionalSpec: /actuator/health, liveness and readiness all
UP (readiness against Testcontainers PostgreSQL), and env/metrics stay closed
- the same readiness endpoint the Compose HEALTHCHECK polls
Assisted-by: claude-code:claude-opus-4-7
---
conf/guides.yml | 2 +
.../v8/guide/testing.adoc | 34 ++++++++++
.../example/ActuatorHealthFunctionalSpec.groovy | 73 ++++++++++++++++++++++
.../src/test/resources/application-test.yml | 8 +++
4 files changed, 117 insertions(+)
diff --git a/conf/guides.yml b/conf/guides.yml
index 895b179da0d..e4db8795db8 100644
--- a/conf/guides.yml
+++ b/conf/guides.yml
@@ -1939,6 +1939,8 @@ guides:
title: Pushing to GitHub Container Registry
nonRoot:
title: Running as a Non-Root User
+ testing:
+ title: Testing the Health Probes
helpWithGrails:
title: Do you need help with Grails?
diff --git a/guides/grails-docker-bootbuildimage/v8/guide/testing.adoc b/guides/grails-docker-bootbuildimage/v8/guide/testing.adoc
new file mode 100644
index 00000000000..355b5444df3
--- /dev/null
+++ b/guides/grails-docker-bootbuildimage/v8/guide/testing.adoc
@@ -0,0 +1,34 @@
+The deployment contract for this app is not a web page - it is the Actuator health surface that the Docker `HEALTHCHECK` and a Kubernetes readiness/liveness probe poll. A functional test pins exactly that contract, booting the app against a Testcontainers PostgreSQL database (the readiness probe checks the datasource, so a real database is required for it to report `UP`).
+
+== Test configuration
+
+Testcontainers artifacts are not in the Grails BOM, so they need an explicit `testcontainers-bom`. Because the app uses the database-migration plugin with an empty changelog, the test profile turns migration off and lets GORM create the schema directly:
+
+[source,yaml]
+.src/test/resources/application-test.yml
+----
+include::../snippets/src/test/resources/application-test.yml[]
+----
+
+== The health-probe functional spec
+
+[source,groovy]
+.src/integration-test/groovy/example/ActuatorHealthFunctionalSpec.groovy
+----
+include::../snippets/src/integration-test/groovy/example/ActuatorHealthFunctionalSpec.groovy[]
+----
+
+The assertions encode the operational contract from the link:#healthCheck[health-probe chapter]:
+
+* `/actuator/health`, `/actuator/health/liveness`, and `/actuator/health/readiness` all report `UP` (the status is nested as `status.code` in Spring Boot's health JSON);
+* the readiness probe reports `UP` only because a real database is reachable - this is why the spec runs against Testcontainers PostgreSQL rather than a mock;
+* the operator-grade endpoints (`/actuator/env`, `/actuator/metrics`) stay closed (`404`), confirming the `exposure.include: health,info` restriction actually holds.
+
+Run it:
+
+[source,bash]
+----
+./gradlew integrationTest # boots the app against Testcontainers PostgreSQL - Docker required
+----
+
+This is the same `readiness` endpoint the Compose `HEALTHCHECK` polls, so a green functional run is direct evidence the container's health check will pass once the image is built and run.
diff --git a/guides/grails-docker-bootbuildimage/v8/snippets/src/integration-test/groovy/example/ActuatorHealthFunctionalSpec.groovy b/guides/grails-docker-bootbuildimage/v8/snippets/src/integration-test/groovy/example/ActuatorHealthFunctionalSpec.groovy
new file mode 100644
index 00000000000..1ce6381da5a
--- /dev/null
+++ b/guides/grails-docker-bootbuildimage/v8/snippets/src/integration-test/groovy/example/ActuatorHealthFunctionalSpec.groovy
@@ -0,0 +1,73 @@
+package example
+
+import grails.testing.mixin.integration.Integration
+import groovy.json.JsonSlurper
+import org.springframework.beans.factory.annotation.Value
+import spock.lang.Specification
+
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+
+/**
+ * Functional tests for the Spring Boot Actuator health surface this guide
+ * configures - the same endpoints the Docker HEALTHCHECK and a Kubernetes
+ * readiness/liveness probe hit. @Integration boots the app against a
+ * Testcontainers PostgreSQL database, so the readiness probe (which checks
+ * the datasource) reports UP.
+ *
+ * No Geb here: the deployment contract is HTTP/JSON, not a browser page.
+ */
+@Integration
+class ActuatorHealthFunctionalSpec extends Specification {
+
+ @Value('${local.server.port}')
+ Integer serverPort
+
+ HttpClient client = HttpClient.newHttpClient()
+
+ private HttpResponse<String> get(String path) {
+ client.send(
+ HttpRequest.newBuilder(URI.create("http://localhost:${serverPort}${path}")).GET().build(),
+ HttpResponse.BodyHandlers.ofString())
+ }
+
+ // Spring Boot's health JSON nests the status as {code, description}, so
+ // the live status string is json.status.code.
+ private static String statusCode(String body) {
+ (new JsonSlurper().parseText(body) as Map).status.code
+ }
+
+ void "GET /actuator/health reports UP"() {
+ when:
+ HttpResponse<String> resp = get('/actuator/health')
+
+ then:
+ resp.statusCode() == 200
+ statusCode(resp.body()) == 'UP'
+ }
+
+ void "the liveness probe is exposed and UP"() {
+ when:
+ HttpResponse<String> resp = get('/actuator/health/liveness')
+
+ then:
+ resp.statusCode() == 200
+ statusCode(resp.body()) == 'UP'
+ }
+
+ void "the readiness probe is exposed and UP against a real database"() {
+ when:
+ HttpResponse<String> resp = get('/actuator/health/readiness')
+
+ then:
+ resp.statusCode() == 200
+ statusCode(resp.body()) == 'UP'
+ }
+
+ void "operator-grade endpoints stay closed"() {
+ expect: 'only health and info are on the public surface'
+ get('/actuator/env').statusCode() == 404
+ get('/actuator/metrics').statusCode() == 404
+ }
+}
diff --git a/guides/grails-docker-bootbuildimage/v8/snippets/src/test/resources/application-test.yml b/guides/grails-docker-bootbuildimage/v8/snippets/src/test/resources/application-test.yml
new file mode 100644
index 00000000000..8d5999fd500
--- /dev/null
+++ b/guides/grails-docker-bootbuildimage/v8/snippets/src/test/resources/application-test.yml
@@ -0,0 +1,8 @@
+dataSource:
+ url: jdbc:tc:postgresql:12:///postgres
+ driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver
+ dbCreate: create-drop
+grails:
+ plugin:
+ databasemigration:
+ updateOnStart: false