Skip to content

Commit

Permalink
Merge pull request #61 from grails/matrei/feat-container-geb-spec
Browse files Browse the repository at this point in the history
Add feature to run Geb specs with Testcontainers
  • Loading branch information
matrei authored Oct 23, 2024
2 parents d02343b + 2061ddf commit 5a302d0
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 64 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@

Geb Functional Testing for the Grails® framework.

This plugin provides the Geb dependencies, a `create-functional-test` command for generating Geb tests in a Grails app
and also adds a ContainerGebSpec that when inherited from automatically runs the browser in a container.

This plugin provides the Geb dependencies and a `create-functional-test` command for generating Geb tests in a Grails app.

It also provides a `ContainerGebSpec` class, which can be used in place of `GebSpec`, that automatically
runs the browser in a container using [Testcontainers](https://java.testcontainers.org/). This requires a
[compatible container runtime](https://java.testcontainers.org/supported_docker_environment/) such as
[Docker](https://www.docker.com/) to be installed.

For further reference please see the [Geb documentation](https://www.gebish.org).

## Examples
Expand Down
76 changes: 39 additions & 37 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,88 +1,90 @@
buildscript {
repositories {
maven { url "https://repo.grails.org/grails/core" }
maven { url = 'https://repo.grails.org/grails/core' }
}
dependencies {
classpath "org.grails:grails-gradle-plugin:$grailsGradlePluginVersion"
}
}

version projectVersion
group "org.grails.plugins"
version = projectVersion
group = 'org.grails.plugins'

apply plugin: 'java-library'
apply plugin: 'idea'
apply plugin: 'java-test-fixtures'
apply plugin: 'org.grails.grails-plugin'
apply plugin: 'org.grails.internal.grails-plugin-publish'
apply plugin: 'maven-publish'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
vendor = JvmVendorSpec.BELLSOFT
}
}

repositories {
maven { url "https://repo.grails.org/grails/core" }
maven { url = 'https://repo.grails.org/grails/core' }
}

configurations {
documentation.extendsFrom compileClasspath
}

dependencies {
compileOnly "org.grails:grails-core:$grailsVersion"

// used transitively by the generated tests
api "org.gebish:geb-spock:$gebSpock"
api "org.grails:grails-testing-support:$testingSupportVersion"
api "org.grails:grails-datastore-gorm:$datastoreVersion"
implementation(platform("org.grails:grails-bom:$grailsVersion"))

documentation "org.apache.groovy:groovy:$groovyVersion"
documentation "org.apache.groovy:groovy-ant:$groovyVersion"
documentation "org.apache.groovy:groovy-templates:$groovyVersion"
documentation "com.github.javaparser:javaparser-core:$javaParserCoreVersion"
}
compileOnly 'org.grails:grails-core' // Provided as this is a Grails plugin

testFixturesCompileOnly 'jakarta.servlet:jakarta.servlet-api'
testFixturesApi 'org.gebish:geb-spock'
testFixturesApi 'org.grails:grails-testing-support'
testFixturesApi 'org.grails:grails-datastore-gorm'
testFixturesApi "org.testcontainers:selenium:$testcontainersVersion"
testFixturesApi "org.seleniumhq.selenium:selenium-chrome-driver:$seleniumVersion"
testFixturesApi "org.seleniumhq.selenium:selenium-remote-driver:$seleniumVersion"

findMainClass.enabled = false
documentation 'org.apache.groovy:groovy'
documentation 'org.apache.groovy:groovy-ant'
documentation 'org.apache.groovy:groovy-templates'
documentation 'com.github.javaparser:javaparser-core'
}

grailsPublish {
userOrg = 'grails'
githubSlug = 'grails/geb'
license {
name = 'Apache-2.0'
}
title = "Grails Geb Plugin"
desc = "Provides Integration with Geb for Functional Testing"
developers = [graemerocher: "Graeme Rocher", puneetbehl: "Puneet Behl"]
title = 'Grails Geb Plugin'
desc = 'Provides Integration with Geb for Functional Testing'
developers = [
graemerocher: 'Graeme Rocher',
puneetbehl: 'Puneet Behl',
sbglasius: 'Søren Berg Glasius',
matrei: 'Mattias Reichel'
]
}

tasks.withType(Groovydoc) {
destinationDir = new File(buildDir, 'docs/api')
tasks.withType(Groovydoc).configureEach {
destinationDir = layout.buildDirectory.dir('docs/api').get().asFile
docTitle = "Grails Geb Plugin ${version}"
classpath = configurations.documentation
}

tasks.withType(GroovyCompile) {
configure(groovyOptions) {
forkOptions.jvmArgs = ['-Xmx1024m']
}
}

tasks.withType(Test) {
tasks.withType(Test).configureEach {
useJUnitPlatform()
}

test {
testLogging {
events "passed", "skipped", "failed"

events 'passed', 'skipped', 'failed'
showExceptions true
exceptionFormat "full"
exceptionFormat 'full'
showCauses true
showStackTraces true
}
}

bootJar.enabled = false
bootRun.enabled = false
bootTestRun.enabled = false
tasks.named('bootJar') { enabled = false }
tasks.named('bootRun') { enabled = false }
tasks.named('bootTestRun') { enabled = false }
tasks.named('findMainClass') { enabled = false }
14 changes: 6 additions & 8 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
projectVersion=5.0.0-SNAPSHOT
title=Grails Geb Plugin
developers=Puneet Behl

datastoreVersion=9.0.0-SNAPSHOT
gebSpock=7.0
grailsVersion=7.0.0-SNAPSHOT
grailsGradlePluginVersion=7.0.0-SNAPSHOT
groovyVersion=4.0.23
javaParserCoreVersion=3.26.2
testingSupportVersion=4.0.0-SNAPSHOT
seleniumVersion=4.25.0
testcontainersVersion=1.20.2

# This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs
# https://github.com/grails/grails-gradle-plugin/issues/222
slf4jPreventExclusion=true

org.gradle.parallel=true
org.gradle.caching=true
Expand Down
33 changes: 15 additions & 18 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
plugins {
id "com.gradle.enterprise" version "3.18.1"
id 'com.gradle.develocity' version '3.18.1'
id 'com.gradle.common-custom-user-data-gradle-plugin' version '2.0.2'
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}

rootProject.name="geb"
def isCI = System.getenv('CI') != null
def isLocal = !isCI
def isAuthenticated = System.getenv('DEVELOCITY_ACCESS_KEY') != null

gradleEnterprise {
develocity {
server = 'https://ge.grails.org'
buildScan {
publishAlways()
publishIfAuthenticated()
uploadInBackground = System.getenv("CI") == null
capture {
taskInputFiles = true
}
publishing.onlyIf { isAuthenticated }
uploadInBackground = isLocal
}
}

buildCache {
local { enabled = System.getenv('CI') != 'true' }
remote(HttpBuildCache) {
push = System.getenv('CI') == 'true'
local { enabled = isLocal }
remote(develocity.buildCache) {
push = isCI && isAuthenticated
enabled = true
url = 'https://ge.grails.org/cache/'
credentials {
username = System.getenv('GRADLE_ENTERPRISE_BUILD_CACHE_NODE_USER')
password = System.getenv('GRADLE_ENTERPRISE_BUILD_CACHE_NODE_KEY')
}
}}
}
}

rootProject.name = 'geb'
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package geb
package grails.plugin.geb

import grails.plugins.Plugin
import grails.plugins.metadata.PluginSource
Expand Down
108 changes: 108 additions & 0 deletions src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2024 original author or authors
*
* Licensed 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 grails.plugin.geb

import geb.spock.GebSpec
import groovy.transform.PackageScope
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.remote.RemoteWebDriver
import org.testcontainers.Testcontainers
import org.testcontainers.containers.BrowserWebDriverContainer
import org.testcontainers.containers.PortForwardingContainer
import spock.lang.Shared

import java.time.Duration

/**
* A {@link geb.spock.GebSpec GebSpec} that leverages Testcontainers to run the browser inside a container.
*
* <p>Prerequisites:
* <ul>
* <li>
* The test class must be annotated with {@link grails.testing.mixin.integration.Integration @Integration}.
* </li>
* <li>
* A <a href="https://java.testcontainers.org/supported_docker_environment/">compatible container runtime</a>
* (e.g., Docker) must be available for Testcontainers to utilize.
* </li>
* </ul>
*
* @author Søren Berg Glasius
* @author Mattias Reichel
* @since 5.0.0
*/
class ContainerGebSpec extends GebSpec {

private static final String DEFAULT_PROTOCOL = 'http'
private static final String DEFAULT_HOSTNAME = 'host.testcontainers.internal'

@Shared
BrowserWebDriverContainer webDriverContainer

@PackageScope
void initialize() {
if (!webDriverContainer) {
if (!hasProperty('serverPort')) {
throw new IllegalStateException('Test class must be annotated with @Integration for serverPort to be injected')
}
webDriverContainer = new BrowserWebDriverContainer()
webDriverContainer.tap {
addExposedPort(serverPort)
withAccessToHost(true)
start()
}
Testcontainers.exposeHostPorts(serverPort)
if (hostName != DEFAULT_HOSTNAME) {
webDriverContainer.execInContainer('/bin/sh', '-c', "echo '$hostIp\t$hostName' | sudo tee -a /etc/hosts")
}
browser.driver = new RemoteWebDriver(webDriverContainer.seleniumAddress, new ChromeOptions())
browser.driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30))
}
}

void setup() {
initialize()
baseUrl = "$protocol://$hostName:$serverPort"
}

def cleanupSpec() {
webDriverContainer.stop()
}

/**
* Returns the protocol that the browser will use to access the server under test.
* <p>Defaults to {@code http}.
*
* @return the protocol for accessing the server under test
*/
String getProtocol() {
return DEFAULT_PROTOCOL
}

/**
* Returns the hostname that the browser will use to access the server under test.
* <p>Defaults to {@code host.testcontainers.internal}.
*
* @return the hostname for accessing the server under test
*/
String getHostName() {
return DEFAULT_HOSTNAME
}

private static String getHostIp() {
PortForwardingContainer.INSTANCE.network.get().ipAddress
}
}

0 comments on commit 5a302d0

Please sign in to comment.