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/03 20:15:09 UTC

(grails-static-website) branch add-grails-spock-test-tour-v8 created (now 8c867315746)

This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a change to branch add-grails-spock-test-tour-v8
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git


      at 8c867315746 Add grails-spock-test-tour v8 guide

This branch includes the following new commits:

     new 8c867315746 Add grails-spock-test-tour v8 guide

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(grails-static-website) 01/01: Add grails-spock-test-tour v8 guide

Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch add-grails-spock-test-tour-v8
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git

commit 8c86731574649c4aa8e2c6f6bdce240514a67654
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sun May 3 16:15:02 2026 -0400

    Add grails-spock-test-tour v8 guide
    
    Working examples of every Spock test layer Grails 8 supports.
    Greenfield - new grails-guides/grails-spock-test-tour repo created public
    on the grails8 default branch.
    
    The 13 chapters demonstrate one canonical example per layer:
    
    - Getting Started (whatYouWillBuild, requirements, howto)
    - Five Test Layers (the speed/cost/coverage matrix)
    - DomainUnitTest for Constraints (BookSpec with @Unroll where: table
      for pageCount)
    - ServiceUnitTest + DataTest for Queries (BookServiceSpec, in-memory
      GORM, dynamic-finder testing)
    - ControllerUnitTest for Routing (BookControllerSpec, response status +
      request.json shape)
    - @Integration With @Rollback (BookIntegrationSpec, real Spring
      context, real datasource, transaction-isolated specs)
    - Geb Functional Tests (Testcontainers Postgres, browser session reuse,
      cross-references the CI/CD guide for staging)
    - Parameterised where: Data Tables (block discipline, mock-call-count
      assertions)
    - useJUnitPlatform and JaCoCo Coverage (the wiring most teams get wrong)
    
    Verified locally:
    
      ./gradlew validateGuides -PvalidationMode=both
        [validateGuides] mode=both: 86 guide(s) parsed, 0 SKIP-warned, 0 errors
    
      ./gradlew renderGuide_grails_spock_test_tour_8 --rerun-tasks --no-configuration-cache
        Renders all 13 chapter HTML pages, no Unresolved directive errors.
    
    Assisted-by: claude-code:claude-opus-4-7
---
 conf/guides.yml                                    | 48 ++++++++++++++++++
 .../v8/guide/controllerUnitTest.adoc               | 13 +++++
 .../grails-spock-test-tour/v8/guide/dataTest.adoc  | 13 +++++
 .../v8/guide/domainUnitTest.adoc                   | 13 +++++
 .../v8/guide/functionalTest.adoc                   | 24 +++++++++
 .../v8/guide/gettingStarted.adoc                   |  3 ++
 .../v8/guide/helpWithGrails.adoc                   |  1 +
 guides/grails-spock-test-tour/v8/guide/howto.adoc  | 10 ++++
 .../v8/guide/integrationTest.adoc                  | 15 ++++++
 .../v8/guide/parameterised.adoc                    | 33 ++++++++++++
 .../grails-spock-test-tour/v8/guide/platform.adoc  | 30 +++++++++++
 .../v8/guide/requirements.adoc                     |  4 ++
 .../v8/guide/testLayers.adoc                       | 16 ++++++
 .../v8/guide/whatYouWillBuild.adoc                 | 12 +++++
 .../controllers/example/BookController.groovy      |  9 ++++
 .../snippets/grails-app/domain/example/Book.groovy | 17 +++++++
 .../grails-app/services/example/BookService.groovy | 12 +++++
 .../groovy/example/BookIntegrationSpec.groovy      | 50 ++++++++++++++++++
 .../test/groovy/example/BookControllerSpec.groovy  | 54 ++++++++++++++++++++
 .../src/test/groovy/example/BookServiceSpec.groovy | 44 ++++++++++++++++
 .../src/test/groovy/example/BookSpec.groovy        | 59 ++++++++++++++++++++++
 21 files changed, 480 insertions(+)

diff --git a/conf/guides.yml b/conf/guides.yml
index 9a0bcf68f2c..35986dc57b6 100644
--- a/conf/guides.yml
+++ b/conf/guides.yml
@@ -3636,6 +3636,54 @@ guides:
           helpWithGrails:
             title: Help with Grails
 
+  - name: 'grails-spock-test-tour'
+    title: 'A Spock Test Tour for Grails 8'
+    subtitle: 'Working examples of every Spock test layer Grails 8 supports - DomainUnitTest, ServiceUnitTest + DataTest, ControllerUnitTest, @Integration + @Rollback, plus parameterised where: data tables.'
+    authors:
+      - 'James Fredley'
+    category: 'Grails Testing'
+    publicationDate: '2026-05-03'
+    versions:
+      '8':
+        sourcePath: guides/grails-spock-test-tour/v8
+        publicationDate: '2026-05-03'
+        tags:
+          - 'spock'
+          - 'testing'
+          - 'gorm'
+          - 'data-test'
+          - 'integration-test'
+          - 'geb'
+          - 'jacoco'
+          - 'grails8'
+        sampleRef:
+          repo: 'grails-guides/grails-spock-test-tour'
+          branch: 'grails8'
+        toc:
+          gettingStarted:
+            title: Getting Started
+            whatYouWillBuild: What You Will Build
+            requirements: What You Will Need
+            howto: How to Complete the Guide
+          testLayers:
+            title: Five Test Layers
+          domainUnitTest:
+            title: DomainUnitTest for Constraints
+          dataTest:
+            title: ServiceUnitTest + DataTest for Queries
+          controllerUnitTest:
+            title: ControllerUnitTest for Routing
+          integrationTest:
+            title: '@Integration With @Rollback'
+          functionalTest:
+            title: Geb Functional Tests
+          parameterised:
+            title: 'Parameterised where: Data Tables'
+          platform:
+            title: useJUnitPlatform and JaCoCo Coverage
+          helpWithGrails:
+            title: Do you need help with Grails?
+
   - name: 'grails-spring-security-core-plugin-custom-authentication'
     title: 'Grails Spring Security Core Plugin Custom Authentication'
     subtitle: 'Shows how to create a custom authentication with Spring Security Core Plugin'
diff --git a/guides/grails-spock-test-tour/v8/guide/controllerUnitTest.adoc b/guides/grails-spock-test-tour/v8/guide/controllerUnitTest.adoc
new file mode 100644
index 00000000000..95a3258e05e
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/controllerUnitTest.adoc
@@ -0,0 +1,13 @@
+`ControllerUnitTest<BookController>` exercises just the controller plus its routing, without booting Spring. Add `DataTest` so the auto-generated `RestfulController` actions can find persisted entities.
+
+[source,groovy]
+.src/test/groovy/example/BookControllerSpec.groovy
+----
+include::../snippets/src/test/groovy/example/BookControllerSpec.groovy[]
+----
+
+Three things to point at:
+
+* `request.method = 'GET'` and `request.json = [...]` are the harness's way of constructing a request without a real HTTP transport. The action runs synchronously; `response` carries the result.
+* `response.status == HttpStatus.OK.value()` (and `HttpStatus.NOT_FOUND`, `HttpStatus.UNPROCESSABLE_ENTITY`) - check the HTTP status enum, not magic numbers. Tests stay readable; status code constants change rarely but the enum names never do.
+* The malformed-ISBN test verifies the 422 surface contract from the link:../grails-rest-library/8/guide/index.html[REST Library guide]'s validation chapter. The same input always returns a structured 422 with the right `errors[].code`.
diff --git a/guides/grails-spock-test-tour/v8/guide/dataTest.adoc b/guides/grails-spock-test-tour/v8/guide/dataTest.adoc
new file mode 100644
index 00000000000..c4810d1c774
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/dataTest.adoc
@@ -0,0 +1,13 @@
+`ServiceUnitTest<BookService>` plus `DataTest` exercises GORM finders against the in-memory datastore. Real GORM queries run; no Spring context, no real database.
+
+[source,groovy]
+.src/test/groovy/example/BookServiceSpec.groovy
+----
+include::../snippets/src/test/groovy/example/BookServiceSpec.groovy[]
+----
+
+Three things to highlight:
+
+* `Class[] getDomainClassesToMock() { [Book] }` is what `DataTest` reads to know which entities to register with the in-memory datastore. List every domain class your spec touches; forget one and you get a `Hibernate could not find entity` error at first use.
+* `setup()` runs before every feature method and seeds the datastore. There is no inter-spec state leak because `DataTest` clears the in-memory store between methods.
+* Dynamic-finder method names like `countByPageCountGreaterThanEquals` are interpreted by GORM at runtime. They are typo-prone; write a test the moment you add one.
diff --git a/guides/grails-spock-test-tour/v8/guide/domainUnitTest.adoc b/guides/grails-spock-test-tour/v8/guide/domainUnitTest.adoc
new file mode 100644
index 00000000000..5157c3857a5
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/domainUnitTest.adoc
@@ -0,0 +1,13 @@
+`DomainUnitTest<Book>` is the cheapest and fastest test layer in Grails. It loads in milliseconds because no Spring context boots; the in-memory datastore is per-spec via `@AutoCleanup`. Every constraint - `blank`, `nullable`, `maxSize`, `unique`, `matches`, `min`, `max` - is tested here.
+
+[source,groovy]
+.src/test/groovy/example/BookSpec.groovy
+----
+include::../snippets/src/test/groovy/example/BookSpec.groovy[]
+----
+
+Three patterns to point at:
+
+* `b.errors.fieldError('title').code == 'blank'` checks the *constraint code*, not the localised message. Codes are stable across releases and locales; messages are not.
+* `@Unroll` plus a `where:` data table collapses what would otherwise be five copy-pasted tests into one parameterised spec. The `#pageCount` token in the test name expands to the actual value, so the IDE's run-tree shows each row separately.
+* No `@Transactional`, no Spring, no JDBC. The whole spec runs in a few hundred milliseconds across all six tests.
diff --git a/guides/grails-spock-test-tour/v8/guide/functionalTest.adoc b/guides/grails-spock-test-tour/v8/guide/functionalTest.adoc
new file mode 100644
index 00000000000..c9465e882b2
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/functionalTest.adoc
@@ -0,0 +1,24 @@
+Functional tests use Geb 8 to drive a real browser against a real booted app. The `geb-with-webdriver-binaries` feature you selected at the forge step downloads matching Chrome/Firefox driver binaries automatically.
+
+[source,groovy]
+----
+@Integration
+class BookFunctionalSpec extends GebSpec {
+
+    void "the book index page lists every book"() {
+        when:
+        go '/books'
+
+        then:
+        $('table.book-list tr').size() > 0
+    }
+}
+----
+
+Three keep-it-fast disciplines:
+
+* *Use Testcontainers' Postgres module per-spec*, not a long-running Postgres. The container start is ~3 s once warm; the trade-off is per-spec database isolation.
+* *Reuse a single browser session* across feature methods in the same spec - Geb does this by default. Re-launching Firefox between every method adds 2 s of overhead per method.
+* *Run unit and integration tests first*, functional last. Failure is then localised: if every functional test fails, it is because the app would not boot, which is an integration-test problem.
+
+The matching link:../grails-github-actions-cicd/8/guide/index.html[CI/CD guide] models this as a four-stage job graph (validation -> unit -> integration -> functional) so a broken unit test does not waste 60 seconds on functional runs.
diff --git a/guides/grails-spock-test-tour/v8/guide/gettingStarted.adoc b/guides/grails-spock-test-tour/v8/guide/gettingStarted.adoc
new file mode 100644
index 00000000000..632e5aacc9c
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/gettingStarted.adoc
@@ -0,0 +1,3 @@
+In this guide you will write working examples of every Spock test layer that Apache Grails 8 supports - from millisecond-fast `DomainUnitTest` constraint specs to full-stack `@Integration` specs that boot a real Spring context. The teaching framing is *which test type catches which class of bug*: constraint regressions belong in domain tests; query bugs in `DataTest`; controller routing in `ControllerUnitTest`; cross-layer wiring in `@Integration` tests; end-to-end UI flows only in funct [...]
+
+This guide targets Apache Grails 8 / Spock 2.3 / JDK 21.
diff --git a/guides/grails-spock-test-tour/v8/guide/helpWithGrails.adoc b/guides/grails-spock-test-tour/v8/guide/helpWithGrails.adoc
new file mode 100644
index 00000000000..e062f614b1a
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/helpWithGrails.adoc
@@ -0,0 +1 @@
+include::{commondir}/common-helpWithGrails.adoc[]
diff --git a/guides/grails-spock-test-tour/v8/guide/howto.adoc b/guides/grails-spock-test-tour/v8/guide/howto.adoc
new file mode 100644
index 00000000000..3dec4e46a99
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/howto.adoc
@@ -0,0 +1,10 @@
+You can either type the code in this guide as you read or skip ahead and clone the finished sample:
+
+[source,bash]
+----
+git clone -b grails8 https://github.com/grails-guides/grails-spock-test-tour.git
+cd grails-spock-test-tour/complete
+./gradlew test integrationTest
+----
+
+`initial/` is a vanilla Grails 8 starter with the `postgres`, `testcontainers`, `geb-with-webdriver-binaries`, and `mockito` features. `complete/` adds the Book domain, the BookService, the BookController, and the four spec files this guide walks through.
diff --git a/guides/grails-spock-test-tour/v8/guide/integrationTest.adoc b/guides/grails-spock-test-tour/v8/guide/integrationTest.adoc
new file mode 100644
index 00000000000..f873e3a0d87
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/integrationTest.adoc
@@ -0,0 +1,15 @@
+`@Integration` boots the full Spring context against a real datasource (whatever the `test` environment in `application.yml` configures - H2 by default). `@Rollback` rolls back every test method's database changes so specs stay isolated.
+
+[source,groovy]
+.src/integration-test/groovy/example/BookIntegrationSpec.groovy
+----
+include::../snippets/src/integration-test/groovy/example/BookIntegrationSpec.groovy[]
+----
+
+Three things to highlight:
+
+* `BookService bookService` is a real Spring-injected bean here, not a unit-test mock. The full GORM stack runs; `service.save()` does a real INSERT, `service.get()` does a real SELECT.
+* `@Rollback` is non-negotiable. Without it, the second test sees the first test's data and the third test sees both. Specs become order-dependent and impossible to debug.
+* The duplicate-ISBN test deliberately catches the constraint failure inside the second `save` call rather than letting it throw. `unique: true` constraint failures surface as `errors.hasErrors()`, not as exceptions, in GORM 7+.
+
+Integration specs run an order of magnitude slower than unit tests (5-20 seconds per spec). Use them only for what unit tests genuinely cannot answer: cross-layer wiring, security filter chains, bootstrap-data correctness.
diff --git a/guides/grails-spock-test-tour/v8/guide/parameterised.adoc b/guides/grails-spock-test-tour/v8/guide/parameterised.adoc
new file mode 100644
index 00000000000..03675cd0699
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/parameterised.adoc
@@ -0,0 +1,33 @@
+Spock's `where:` data tables turn N copy-pasted tests into one parameterised feature method. Reach for them whenever a test follows the shape "for input X, expect output Y", with N values of X.
+
+The `BookSpec` already contains one:
+
+[source,groovy]
+----
+@Unroll
+void "pageCount #pageCount is #expectedValid"() {
+    when:
+    Book b = new Book(title: 'X', isbn: '9780547928227', pageCount: pageCount)
+
+    then:
+    b.validate() == expectedValid
+
+    where:
+    pageCount || expectedValid
+    null      || true
+    1         || true
+    100       || true
+    0         || false
+    -1        || false
+}
+----
+
+Two patterns worth pointing at:
+
+* `@Unroll` makes the IDE's run-tree show every row as a separate test. Without it, the five rows show up as one combined test that either passes or fails.
+* `||` separates inputs from expected outputs. Spock allows `|` everywhere, but the convention `inputs || expected_outputs` is widely-followed and reads naturally.
+
+The other two Spock features that pay back disproportionately:
+
+* *Block discipline*: `given:` / `when:` / `then:` / `expect:`. A spec that uses `expect:` for one-liners and `given/when/then` for everything else is dramatically more readable than one that uses `def setup()` and assertions scattered everywhere.
+* *Mock interaction count*: `1 * service.save(_)` asserts the mock was called exactly once with any argument. `0 * _` asserts no further interactions. These are far more precise than verifying state after the fact.
diff --git a/guides/grails-spock-test-tour/v8/guide/platform.adoc b/guides/grails-spock-test-tour/v8/guide/platform.adoc
new file mode 100644
index 00000000000..e8333d8870f
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/platform.adoc
@@ -0,0 +1,30 @@
+Spock 2.x runs on the JUnit Platform. Grails 8's forge-generated `build.gradle` already does the right thing:
+
+[source,groovy]
+----
+tasks.withType(Test).configureEach {
+    useJUnitPlatform()
+}
+----
+
+That single line tells Gradle to invoke the JUnit 5 runner instead of the legacy `JUnit3 / 4` runner. Without it, Spock 2 specs would not be discovered and only legacy JUnit 4 specs would run.
+
+Coverage with JaCoCo is one block:
+
+[source,groovy]
+----
+plugins {
+    id 'jacoco'
+}
+
+jacocoTestReport {
+    dependsOn test, integrationTest
+    executionData fileTree(buildDir).include('jacoco/*.exec')
+    reports {
+        xml.required = true       // for Codecov upload
+        html.required = true      // for human inspection
+    }
+}
+----
+
+The `dependsOn test, integrationTest` line is what makes the report cover both layers; without it, only `test` data is included and integration-only branches show as untested. The `xml.required = true` produces the `jacoco.xml` that Codecov reads from `build/reports/jacoco/test/jacocoTestReport.xml`.
diff --git a/guides/grails-spock-test-tour/v8/guide/requirements.adoc b/guides/grails-spock-test-tour/v8/guide/requirements.adoc
new file mode 100644
index 00000000000..27148f50b79
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/requirements.adoc
@@ -0,0 +1,4 @@
+To complete this guide you will need:
+
+* JDK 21
+* About 30 minutes
diff --git a/guides/grails-spock-test-tour/v8/guide/testLayers.adoc b/guides/grails-spock-test-tour/v8/guide/testLayers.adoc
new file mode 100644
index 00000000000..9041b46720c
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/testLayers.adoc
@@ -0,0 +1,16 @@
+Grails 8 supports five distinct test types. Each catches a different class of bug at a different speed:
+
+[cols="1,3,1,3"]
+|===
+| Layer | What it tests | Speed | When to reach for it
+
+| *Domain unit test*       | Single domain class, constraints + transient logic     | <100 ms | Constraint regressions; pure domain methods
+| *Service / Data unit test* | GORM finders, criteria, where queries against in-memory store | <500 ms | Query-shape bugs; service business logic
+| *Controller unit test*   | Action routing, model preparation, response status     | <500 ms | Routing regressions; ``@Validateable`` command-object binding
+| *Integration test*       | Real Spring context, real datasource, transaction boundaries | 5-20 s | Cross-layer wiring; security filter chains; bootstrap data
+| *Functional test*        | Real booted app, real browser via Geb                    | 10-60 s | End-to-end UI flows; JavaScript interactions
+|===
+
+The discipline is *put each bug in the cheapest test type that can catch it*. A failing constraint should never reveal itself in a 30-second functional run. A controller routing typo should never need a real Postgres.
+
+The next chapters walk through one canonical example per layer, sized so the file stays under 80 lines.
diff --git a/guides/grails-spock-test-tour/v8/guide/whatYouWillBuild.adoc b/guides/grails-spock-test-tour/v8/guide/whatYouWillBuild.adoc
new file mode 100644
index 00000000000..70b055aaac4
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/guide/whatYouWillBuild.adoc
@@ -0,0 +1,12 @@
+By the end of the guide your sample app will have:
+
+* `Book.groovy` - a small domain class with `title`, `isbn` (regex-validated, unique), and `pageCount` (positive integer).
+* `BookService.groovy` - an `@Service(Book)` GORM data service with `findByIsbn`, `countByPageCountGreaterThanEquals`, and the standard CRUD methods.
+* `BookController.groovy` - a `RestfulController<Book>` so the JSON CRUD surface is testable from `ControllerUnitTest`.
+* Four spec files exercising four test layers:
+** `BookSpec` - `DomainUnitTest<Book>`, constraint validation including a `@Unroll` parameterised `where:` table.
+** `BookServiceSpec` - `ServiceUnitTest<BookService>` + `DataTest`, query-shape testing against the in-memory datastore.
+** `BookControllerSpec` - `ControllerUnitTest<BookController>` + `DataTest`, action routing and response status testing.
+** `BookIntegrationSpec` - `@Integration` + `@Rollback`, full-stack tests with a real Spring context.
+
+You will also touch the JVM-level concerns most teams get wrong: `useJUnitPlatform()` wiring, JaCoCo coverage report shape, and how to keep functional tests fast.
diff --git a/guides/grails-spock-test-tour/v8/snippets/grails-app/controllers/example/BookController.groovy b/guides/grails-spock-test-tour/v8/snippets/grails-app/controllers/example/BookController.groovy
new file mode 100644
index 00000000000..4c8df2ca0a8
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/snippets/grails-app/controllers/example/BookController.groovy
@@ -0,0 +1,9 @@
+package example
+
+import grails.rest.RestfulController
+
+class BookController extends RestfulController<Book> {
+    static responseFormats = ['json']
+    BookService bookService
+    BookController() { super(Book) }
+}
diff --git a/guides/grails-spock-test-tour/v8/snippets/grails-app/domain/example/Book.groovy b/guides/grails-spock-test-tour/v8/snippets/grails-app/domain/example/Book.groovy
new file mode 100644
index 00000000000..1fc72a2eefb
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/snippets/grails-app/domain/example/Book.groovy
@@ -0,0 +1,17 @@
+package example
+
+import grails.persistence.Entity
+
+@Entity
+class Book {
+
+    String  title
+    String  isbn
+    Integer pageCount
+
+    static constraints = {
+        title     blank: false, maxSize: 255
+        isbn      blank: false, unique: true, matches: /^(97(8|9))?\d{9}(\d|X)$/
+        pageCount nullable: true, min: 1
+    }
+}
diff --git a/guides/grails-spock-test-tour/v8/snippets/grails-app/services/example/BookService.groovy b/guides/grails-spock-test-tour/v8/snippets/grails-app/services/example/BookService.groovy
new file mode 100644
index 00000000000..e1e678654fa
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/snippets/grails-app/services/example/BookService.groovy
@@ -0,0 +1,12 @@
+package example
+
+import grails.gorm.services.Service
+
+@Service(Book)
+interface BookService {
+    Book   get(Serializable id)
+    Book   save(Book book)
+    Long   countByPageCountGreaterThanEquals(Integer threshold)
+    Book   findByIsbn(String isbn)
+    List<Book> list(Map args)
+}
diff --git a/guides/grails-spock-test-tour/v8/snippets/src/integration-test/groovy/example/BookIntegrationSpec.groovy b/guides/grails-spock-test-tour/v8/snippets/src/integration-test/groovy/example/BookIntegrationSpec.groovy
new file mode 100644
index 00000000000..289c130b9f2
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/snippets/src/integration-test/groovy/example/BookIntegrationSpec.groovy
@@ -0,0 +1,50 @@
+package example
+
+import grails.gorm.transactions.Rollback
+import grails.testing.mixin.integration.Integration
+import spock.lang.Specification
+
+/**
+ * @Integration boots the full Spring context against a real datasource
+ * (whatever the `test` environment in application.yml configures - H2
+ * by default). @Rollback ensures every test method's database changes
+ * are rolled back at the end so specs stay isolated.
+ *
+ * Use this layer for cross-layer concerns: domain + service + transaction
+ * boundary together. Avoid it for what unit tests can answer faster.
+ */
+@Integration
+@Rollback
+class BookIntegrationSpec extends Specification {
+
+    BookService bookService
+
+    void "a saved book round-trips through GORM"() {
+        given:
+        Book draft = new Book(title: 'Round Trip', isbn: '9780547928227', pageCount: 200)
+
+        when:
+        Book saved = bookService.save(draft)
+
+        then:
+        saved.id != null
+
+        when:
+        Book reloaded = bookService.get(saved.id)
+
+        then:
+        reloaded.title == 'Round Trip'
+        reloaded.isbn  == '9780547928227'
+    }
+
+    void "saving a book with a duplicate isbn fails validation"() {
+        given:
+        bookService.save(new Book(title: 'First',  isbn: '9780547928227', pageCount: 100))
+
+        when:
+        Book second = bookService.save(new Book(title: 'Second', isbn: '9780547928227', pageCount: 200))
+
+        then:
+        second == null || second.hasErrors()
+    }
+}
diff --git a/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookControllerSpec.groovy b/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookControllerSpec.groovy
new file mode 100644
index 00000000000..0b7a71cc136
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookControllerSpec.groovy
@@ -0,0 +1,54 @@
+package example
+
+import grails.testing.gorm.DataTest
+import grails.testing.web.controllers.ControllerUnitTest
+import org.springframework.http.HttpStatus
+import spock.lang.Specification
+
+/**
+ * ControllerUnitTest<BookController> wires up just enough of the web
+ * layer to exercise action routing, model preparation, and response
+ * status codes. Adding DataTest (with the right domain mocked) lets
+ * the RestfulController's auto-generated CRUD actions actually find
+ * persisted entities through GORM.
+ */
+class BookControllerSpec extends Specification
+        implements ControllerUnitTest<BookController>, DataTest {
+
+    Class[] getDomainClassesToMock() { [Book] }
+
+    void "GET /books/{id} returns the book as JSON"() {
+        given:
+        Book b = new Book(title: 'The Hobbit', isbn: '9780547928227', pageCount: 310)
+                  .save(failOnError: true, flush: true)
+
+        when:
+        request.method = 'GET'
+        controller.show(b.id)
+
+        then:
+        response.status == HttpStatus.OK.value()
+        response.json.title == 'The Hobbit'
+    }
+
+    void "GET /books/{id} for a missing id returns 404"() {
+        when:
+        request.method = 'GET'
+        controller.show(99999L)
+
+        then:
+        response.status == HttpStatus.NOT_FOUND.value()
+    }
+
+    void "POST /books with a malformed ISBN returns 422"() {
+        given:
+        request.method = 'POST'
+        request.json = [title: 'X', isbn: 'not-an-isbn']
+
+        when:
+        controller.save()
+
+        then:
+        response.status == HttpStatus.UNPROCESSABLE_ENTITY.value()
+    }
+}
diff --git a/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookServiceSpec.groovy b/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookServiceSpec.groovy
new file mode 100644
index 00000000000..0dc77fe887e
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookServiceSpec.groovy
@@ -0,0 +1,44 @@
+package example
+
+import grails.testing.gorm.DataTest
+import grails.testing.services.ServiceUnitTest
+import spock.lang.Specification
+
+/**
+ * DataTest + ServiceUnitTest exercises the @Service(Book) interface
+ * against the in-memory datastore. Real GORM finders run; no Spring
+ * context, no real database. Best fit for testing query logic
+ * (criteria, where queries, finder method names).
+ */
+class BookServiceSpec extends Specification
+        implements ServiceUnitTest<BookService>, DataTest {
+
+    Class[] getDomainClassesToMock() { [Book] }
+
+    void setup() {
+        new Book(title: 'Short Story',  isbn: '9780000000001', pageCount: 50 ).save(failOnError: true)
+        new Book(title: 'Novella',      isbn: '9780000000002', pageCount: 150).save(failOnError: true)
+        new Book(title: 'Novel',        isbn: '9780000000003', pageCount: 400).save(failOnError: true)
+        new Book(title: 'Doorstopper',  isbn: '9780000000004', pageCount: 900).save(failOnError: true)
+    }
+
+    void "countByPageCountGreaterThanEquals returns the right count"() {
+        expect:
+        service.countByPageCountGreaterThanEquals(200) == 2
+        service.countByPageCountGreaterThanEquals(0)   == 4
+        service.countByPageCountGreaterThanEquals(901) == 0
+    }
+
+    void "findByIsbn returns the matching book"() {
+        when:
+        Book b = service.findByIsbn('9780000000003')
+
+        then:
+        b.title == 'Novel'
+    }
+
+    void "findByIsbn returns null when no match"() {
+        expect:
+        service.findByIsbn('9999999999999') == null
+    }
+}
diff --git a/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookSpec.groovy b/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookSpec.groovy
new file mode 100644
index 00000000000..05781e851a6
--- /dev/null
+++ b/guides/grails-spock-test-tour/v8/snippets/src/test/groovy/example/BookSpec.groovy
@@ -0,0 +1,59 @@
+package example
+
+import grails.testing.gorm.DomainUnitTest
+import spock.lang.Specification
+import spock.lang.Unroll
+
+/**
+ * DomainUnitTest<Book> exercises just the constraint logic.
+ *
+ * Loads in milliseconds because no Spring context boots; the
+ * @AutoCleanup datastore is in-memory and per-spec. Use this layer for
+ * every constraint regression: blank, nullable, maxSize, unique,
+ * matches, min, max, range.
+ */
+class BookSpec extends Specification implements DomainUnitTest<Book> {
+
+    void "rejects a book with no title"() {
+        when:
+        Book b = new Book(isbn: '9780547928227', pageCount: 310)
+
+        then:
+        !b.validate()
+        b.errors.fieldError('title').code == 'blank'
+    }
+
+    void "rejects a book with a malformed ISBN"() {
+        when:
+        Book b = new Book(title: 'Test', isbn: 'not-an-isbn', pageCount: 100)
+
+        then:
+        !b.validate()
+        b.errors.fieldError('isbn').code == 'matches.invalid'
+    }
+
+    void "accepts a book with a valid ISBN-13"() {
+        when:
+        Book b = new Book(title: 'The Hobbit', isbn: '9780547928227', pageCount: 310)
+
+        then:
+        b.validate()
+    }
+
+    @Unroll
+    void "pageCount #pageCount is #expectedValid"() {
+        when:
+        Book b = new Book(title: 'X', isbn: '9780547928227', pageCount: pageCount)
+
+        then:
+        b.validate() == expectedValid
+
+        where:
+        pageCount || expectedValid
+        null      || true
+        1         || true
+        100       || true
+        0         || false
+        -1        || false
+    }
+}