Skip to content

Commit

Permalink
Support VNC recording with BrowserWebdriverContainer in Spock-Extensi…
Browse files Browse the repository at this point in the history
…on (testcontainers#2548)

Initially done as part of the Hackergarten at Gr8ConfEU 2018.
Thanks to Tamer Shahin, Marcin Erdmann and Dawid Kublik for working on this!

Fixes #631.
  • Loading branch information
kiview authored and quincy committed May 28, 2020
1 parent 60d6cbb commit 4c6e664
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 4 deletions.
2 changes: 2 additions & 0 deletions modules/spock/build.gradle
Expand Up @@ -8,8 +8,10 @@ dependencies {
compile project(':testcontainers')
compile 'org.spockframework:spock-core:1.3-groovy-2.5'

testCompile project(':selenium')
testCompile project(':mysql')
testCompile project(':postgresql')

testCompile 'com.zaxxer:HikariCP:3.4.2'
testCompile 'org.apache.httpcomponents:httpclient:4.5.12'

Expand Down
@@ -0,0 +1,36 @@
package org.testcontainers.spock

import groovy.transform.PackageScope
import org.spockframework.runtime.extension.IMethodInvocation
import org.testcontainers.lifecycle.TestDescription

/**
* Spock specific implementation of a Testcontainers TestDescription.
*
* Filesystem friendly name is based on Specification and Feature.
*/
@PackageScope
class SpockTestDescription implements TestDescription {

String specName
String featureName

static SpockTestDescription fromTestDescription(IMethodInvocation invocation) {
return new SpockTestDescription([
specName: invocation.spec.name,
featureName: invocation.feature.name
])
}

@Override
String getTestId() {
return getFilesystemFriendlyName()
}

@Override
String getFilesystemFriendlyName() {
return [specName, featureName].collect {
URLEncoder.encode(it, 'UTF-8')
}.join('-')
}
}
@@ -1,17 +1,32 @@
package org.testcontainers.spock

import org.spockframework.runtime.AbstractRunListener
import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension
import org.spockframework.runtime.model.ErrorInfo
import org.spockframework.runtime.model.SpecInfo

class TestcontainersExtension extends AbstractAnnotationDrivenExtension<Testcontainers> {

@Override
void visitSpecAnnotation(Testcontainers annotation, SpecInfo spec) {
def interceptor = new TestcontainersMethodInterceptor(spec)
def listener = new ErrorListener()
def interceptor = new TestcontainersMethodInterceptor(spec, listener)
spec.addSetupSpecInterceptor(interceptor)
spec.addCleanupSpecInterceptor(interceptor)
spec.addSetupInterceptor(interceptor)
spec.addCleanupInterceptor(interceptor)

spec.addListener(listener)

}

private class ErrorListener extends AbstractRunListener {
List<ErrorInfo> errors = []

@Override
void error(ErrorInfo error) {
errors.add(error)
}
}

}
Expand Up @@ -6,13 +6,17 @@ import org.spockframework.runtime.model.FieldInfo
import org.spockframework.runtime.model.SpecInfo
import org.testcontainers.containers.DockerComposeContainer
import org.testcontainers.containers.GenericContainer
import org.testcontainers.lifecycle.TestLifecycleAware
import org.testcontainers.spock.TestcontainersExtension.ErrorListener

class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {

private final SpecInfo spec
private final ErrorListener errorListener

TestcontainersMethodInterceptor(SpecInfo spec) {
TestcontainersMethodInterceptor(SpecInfo spec, ErrorListener errorListener) {
this.spec = spec
this.errorListener = errorListener
}

@Override
Expand Down Expand Up @@ -50,6 +54,13 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {

@Override
void interceptCleanupMethod(IMethodInvocation invocation) throws Throwable {
findAllTestLifecycleAwareContainers(invocation).each {
// we assume first error is the one we want
def maybeException = Optional.ofNullable(errorListener.errors[0]?.exception)
def testDescription = SpockTestDescription.fromTestDescription(invocation)
it.afterTest(testDescription, maybeException)
}

def containers = findAllContainers(false)
stopContainers(containers, invocation)

Expand All @@ -65,6 +76,14 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {
}
}

private List<TestLifecycleAware> findAllTestLifecycleAwareContainers(IMethodInvocation invocation) {
spec.allFields.findAll { FieldInfo f ->
TestLifecycleAware.isAssignableFrom(f.type)
}.collect {
it.readValue(invocation.instance) as TestLifecycleAware
}
}

private List<FieldInfo> findAllComposeContainers(boolean shared) {
spec.allFields.findAll { FieldInfo f ->
DockerComposeContainer.isAssignableFrom(f.type) && f.shared == shared
Expand All @@ -90,14 +109,14 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {
private static void startComposeContainers(List<FieldInfo> compose, IMethodInvocation invocation) {
compose.each { FieldInfo f ->
DockerComposeContainer c = f.readValue(invocation.instance) as DockerComposeContainer
c.starting(null)
c.start()
}
}

private static void stopComposeContainers(List<FieldInfo> compose, IMethodInvocation invocation) {
compose.each { FieldInfo f ->
DockerComposeContainer c = f.readValue(invocation.instance) as DockerComposeContainer
c.finished(null)
c.stop()
}
}

Expand Down
@@ -0,0 +1,88 @@
package org.testcontainers.spock

import org.intellij.lang.annotations.Language
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification
import spock.lang.Unroll
import spock.util.EmbeddedSpecRunner

import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL
import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_FAILING

class BrowserVncRecordingIT extends Specification {

@Rule
TemporaryFolder temp

String recordingDir

def setup() {
recordingDir = temp.getRoot().getAbsolutePath()
}

@Unroll("For recording mode #recordingMode and failing test is #fails records video file named '#videoFileName'")
def "retains all recordings for RECORD_ALL if successful"() {
given:

//noinspection GrPackage
@Language("groovy")
String myTest = """
package org.testcontainers.spock
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.remote.RemoteWebDriver
import org.testcontainers.containers.BrowserWebDriverContainer
import spock.lang.Specification
import java.util.concurrent.TimeUnit
import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode
@Testcontainers
class BrowserWebdriverContainerIT extends Specification {
BrowserWebDriverContainer browserContainer = new BrowserWebDriverContainer()
.withCapabilities(new ChromeOptions())
.withRecordingMode("$recordingMode" as VncRecordingMode, new File("$recordingDir"))
RemoteWebDriver driver
def setup() {
driver = browserContainer.getWebDriver()
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS)
}
def "should record"() {
when:
driver.get("http://en.wikipedia.org/wiki/Randomness")
then:
driver.findElementByPartialLinkText("pattern").isDisplayed() == !$fails
}
}
"""

when:
EmbeddedSpecRunner runner = new EmbeddedSpecRunner(throwFailure: false)
runner.run(myTest)

then:
def videoDir = temp.getRoot().list()
if (videoFileName.isEmpty()) {
videoDir.length == 0
} else {
videoDir.find { it.contains(videoFileName) }
}

where:
recordingMode | fails | videoFileName
RECORD_ALL | false | 'BrowserWebdriverContainerIT-should+record'
RECORD_FAILING | false | ''
RECORD_FAILING | true | 'BrowserWebdriverContainerIT-should+record'
}

}

0 comments on commit 4c6e664

Please sign in to comment.