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