Skip to content

Commit 631a3f4

Browse files
authored
Support VNC recording with BrowserWebdriverContainer in Spock-Extension (#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.
1 parent 8dede04 commit 631a3f4

File tree

5 files changed

+164
-4
lines changed

5 files changed

+164
-4
lines changed

modules/spock/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ dependencies {
88
compile project(':testcontainers')
99
compile 'org.spockframework:spock-core:1.3-groovy-2.5'
1010

11+
testCompile project(':selenium')
1112
testCompile project(':mysql')
1213
testCompile project(':postgresql')
14+
1315
testCompile 'com.zaxxer:HikariCP:3.4.2'
1416
testCompile 'org.apache.httpcomponents:httpclient:4.5.12'
1517

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.testcontainers.spock
2+
3+
import groovy.transform.PackageScope
4+
import org.spockframework.runtime.extension.IMethodInvocation
5+
import org.testcontainers.lifecycle.TestDescription
6+
7+
/**
8+
* Spock specific implementation of a Testcontainers TestDescription.
9+
*
10+
* Filesystem friendly name is based on Specification and Feature.
11+
*/
12+
@PackageScope
13+
class SpockTestDescription implements TestDescription {
14+
15+
String specName
16+
String featureName
17+
18+
static SpockTestDescription fromTestDescription(IMethodInvocation invocation) {
19+
return new SpockTestDescription([
20+
specName: invocation.spec.name,
21+
featureName: invocation.feature.name
22+
])
23+
}
24+
25+
@Override
26+
String getTestId() {
27+
return getFilesystemFriendlyName()
28+
}
29+
30+
@Override
31+
String getFilesystemFriendlyName() {
32+
return [specName, featureName].collect {
33+
URLEncoder.encode(it, 'UTF-8')
34+
}.join('-')
35+
}
36+
}
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
package org.testcontainers.spock
22

3+
import org.spockframework.runtime.AbstractRunListener
34
import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension
5+
import org.spockframework.runtime.model.ErrorInfo
46
import org.spockframework.runtime.model.SpecInfo
57

68
class TestcontainersExtension extends AbstractAnnotationDrivenExtension<Testcontainers> {
79

810
@Override
911
void visitSpecAnnotation(Testcontainers annotation, SpecInfo spec) {
10-
def interceptor = new TestcontainersMethodInterceptor(spec)
12+
def listener = new ErrorListener()
13+
def interceptor = new TestcontainersMethodInterceptor(spec, listener)
1114
spec.addSetupSpecInterceptor(interceptor)
1215
spec.addCleanupSpecInterceptor(interceptor)
1316
spec.addSetupInterceptor(interceptor)
1417
spec.addCleanupInterceptor(interceptor)
18+
19+
spec.addListener(listener)
20+
21+
}
22+
23+
private class ErrorListener extends AbstractRunListener {
24+
List<ErrorInfo> errors = []
25+
26+
@Override
27+
void error(ErrorInfo error) {
28+
errors.add(error)
29+
}
1530
}
1631

1732
}

modules/spock/src/main/groovy/org/testcontainers/spock/TestcontainersMethodInterceptor.groovy

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import org.spockframework.runtime.model.FieldInfo
66
import org.spockframework.runtime.model.SpecInfo
77
import org.testcontainers.containers.DockerComposeContainer
88
import org.testcontainers.containers.GenericContainer
9+
import org.testcontainers.lifecycle.TestLifecycleAware
10+
import org.testcontainers.spock.TestcontainersExtension.ErrorListener
911

1012
class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {
1113

1214
private final SpecInfo spec
15+
private final ErrorListener errorListener
1316

14-
TestcontainersMethodInterceptor(SpecInfo spec) {
17+
TestcontainersMethodInterceptor(SpecInfo spec, ErrorListener errorListener) {
1518
this.spec = spec
19+
this.errorListener = errorListener
1620
}
1721

1822
@Override
@@ -50,6 +54,13 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {
5054

5155
@Override
5256
void interceptCleanupMethod(IMethodInvocation invocation) throws Throwable {
57+
findAllTestLifecycleAwareContainers(invocation).each {
58+
// we assume first error is the one we want
59+
def maybeException = Optional.ofNullable(errorListener.errors[0]?.exception)
60+
def testDescription = SpockTestDescription.fromTestDescription(invocation)
61+
it.afterTest(testDescription, maybeException)
62+
}
63+
5364
def containers = findAllContainers(false)
5465
stopContainers(containers, invocation)
5566

@@ -65,6 +76,14 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {
6576
}
6677
}
6778

79+
private List<TestLifecycleAware> findAllTestLifecycleAwareContainers(IMethodInvocation invocation) {
80+
spec.allFields.findAll { FieldInfo f ->
81+
TestLifecycleAware.isAssignableFrom(f.type)
82+
}.collect {
83+
it.readValue(invocation.instance) as TestLifecycleAware
84+
}
85+
}
86+
6887
private List<FieldInfo> findAllComposeContainers(boolean shared) {
6988
spec.allFields.findAll { FieldInfo f ->
7089
DockerComposeContainer.isAssignableFrom(f.type) && f.shared == shared
@@ -90,14 +109,14 @@ class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {
90109
private static void startComposeContainers(List<FieldInfo> compose, IMethodInvocation invocation) {
91110
compose.each { FieldInfo f ->
92111
DockerComposeContainer c = f.readValue(invocation.instance) as DockerComposeContainer
93-
c.starting(null)
112+
c.start()
94113
}
95114
}
96115

97116
private static void stopComposeContainers(List<FieldInfo> compose, IMethodInvocation invocation) {
98117
compose.each { FieldInfo f ->
99118
DockerComposeContainer c = f.readValue(invocation.instance) as DockerComposeContainer
100-
c.finished(null)
119+
c.stop()
101120
}
102121
}
103122

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package org.testcontainers.spock
2+
3+
import org.intellij.lang.annotations.Language
4+
import org.junit.Rule
5+
import org.junit.rules.TemporaryFolder
6+
import spock.lang.Specification
7+
import spock.lang.Unroll
8+
import spock.util.EmbeddedSpecRunner
9+
10+
import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL
11+
import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_FAILING
12+
13+
class BrowserVncRecordingIT extends Specification {
14+
15+
@Rule
16+
TemporaryFolder temp
17+
18+
String recordingDir
19+
20+
def setup() {
21+
recordingDir = temp.getRoot().getAbsolutePath()
22+
}
23+
24+
@Unroll("For recording mode #recordingMode and failing test is #fails records video file named '#videoFileName'")
25+
def "retains all recordings for RECORD_ALL if successful"() {
26+
given:
27+
28+
//noinspection GrPackage
29+
@Language("groovy")
30+
String myTest = """
31+
package org.testcontainers.spock
32+
33+
import org.openqa.selenium.chrome.ChromeOptions
34+
import org.openqa.selenium.remote.RemoteWebDriver
35+
import org.testcontainers.containers.BrowserWebDriverContainer
36+
import spock.lang.Specification
37+
38+
import java.util.concurrent.TimeUnit
39+
40+
import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode
41+
42+
@Testcontainers
43+
class BrowserWebdriverContainerIT extends Specification {
44+
45+
BrowserWebDriverContainer browserContainer = new BrowserWebDriverContainer()
46+
.withCapabilities(new ChromeOptions())
47+
.withRecordingMode("$recordingMode" as VncRecordingMode, new File("$recordingDir"))
48+
49+
RemoteWebDriver driver
50+
51+
def setup() {
52+
driver = browserContainer.getWebDriver()
53+
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS)
54+
}
55+
56+
def "should record"() {
57+
when:
58+
driver.get("http://en.wikipedia.org/wiki/Randomness")
59+
60+
then:
61+
driver.findElementByPartialLinkText("pattern").isDisplayed() == !$fails
62+
}
63+
64+
}
65+
66+
67+
"""
68+
69+
when:
70+
EmbeddedSpecRunner runner = new EmbeddedSpecRunner(throwFailure: false)
71+
runner.run(myTest)
72+
73+
then:
74+
def videoDir = temp.getRoot().list()
75+
if (videoFileName.isEmpty()) {
76+
videoDir.length == 0
77+
} else {
78+
videoDir.find { it.contains(videoFileName) }
79+
}
80+
81+
where:
82+
recordingMode | fails | videoFileName
83+
RECORD_ALL | false | 'BrowserWebdriverContainerIT-should+record'
84+
RECORD_FAILING | false | ''
85+
RECORD_FAILING | true | 'BrowserWebdriverContainerIT-should+record'
86+
}
87+
88+
}

0 commit comments

Comments
 (0)