Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ sudo: required
services:
- docker

before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- '$HOME/.gradle'
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/

install:
- ./gradlew build -x check
Expand Down Expand Up @@ -40,7 +44,7 @@ jobs:
-v "$(pwd)":"$(pwd)" \
-w "$(pwd)" \
openjdk:8-jdk-alpine \
./gradlew testcontainers:test --tests '*GenericContainerRuleTest'
./gradlew --no-daemon testcontainers:test --tests '*GenericContainerRuleTest'

- stage: deploy
sudo: false
Expand Down
48 changes: 48 additions & 0 deletions modules/spock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# TestContainers-Spock
[Spock](https://github.com/spockframework/spock) extension for [TestContainers](https://github.com/testcontainers/testcontainers-java) library, which allows to use Docker containers inside of Spock tests.

# Usage

## @Testcontainers class-annotation

Specifying the `@Testcontainers` annotation will instruct Spock to start and stop all testcontainers accordingly. This annotation
can be mixed with Spock's `@Shared` annotation to indicate, that containers shouldn't be restarted between tests.

```groovy
@Testcontainers
class DatabaseTest extends Specification {

@Shared
PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
.withDatabaseName("foo")
.withUsername("foo")
.withPassword("secret")

def "database is accessible"() {

given: "a jdbc connection"
HikariConfig hikariConfig = new HikariConfig()
hikariConfig.setJdbcUrl(postgreSQLContainer.jdbcUrl)
hikariConfig.setUsername("foo")
hikariConfig.setPassword("secret")
HikariDataSource ds = new HikariDataSource(hikariConfig)

when: "querying the database"
Statement statement = ds.getConnection().createStatement()
statement.execute("SELECT 1")
ResultSet resultSet = statement.getResultSet()
resultSet.next()

then: "result is returned"
int resultSetInt = resultSet.getInt(1)
resultSetInt == 1
}
}
```

## General TestContainers usage

See the [TestContainers documentation](https://www.testcontainers.org/) for more information about the underlying library.

# Attributions
The initial version of this project was heavily inspired by the excellent [JUnit5 docker extension](https://github.com/FaustXVI/junit5-docker) by [FaustXVI](https://github.com/FaustXVI).
17 changes: 17 additions & 0 deletions modules/spock/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
plugins {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new plugins DSL, we can of course use also the old one to keep styles more in sync?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use the new style as much as possible, I would keep it :)

id 'groovy'
}

description = "Testcontainers :: Spock-Extension"

dependencies {
compile project(':testcontainers')
compile 'org.spockframework:spock-core:1.0-groovy-2.4'

testCompile project(':mysql')
testCompile project(':postgresql')
testCompile 'com.zaxxer:HikariCP:2.6.1'

testRuntime 'org.postgresql:postgresql:42.0.0'
testRuntime 'mysql:mysql-connector-java:6.0.6'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.testcontainers.spock

import org.spockframework.runtime.extension.ExtensionAnnotation

import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.TYPE, ElementType.METHOD])
@ExtensionAnnotation(TestcontainersExtension)
@interface Testcontainers {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.testcontainers.spock

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

class TestcontainersExtension extends AbstractAnnotationDrivenExtension<Testcontainers> {

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.testcontainers.spock

import org.spockframework.runtime.extension.AbstractMethodInterceptor
import org.spockframework.runtime.extension.IMethodInvocation
import org.spockframework.runtime.model.FieldInfo
import org.spockframework.runtime.model.SpecInfo
import org.testcontainers.containers.DockerComposeContainer
import org.testcontainers.containers.GenericContainer

class TestcontainersMethodInterceptor extends AbstractMethodInterceptor {

private final SpecInfo spec

TestcontainersMethodInterceptor(SpecInfo spec) {
this.spec = spec
}

@Override
void interceptSetupSpecMethod(IMethodInvocation invocation) throws Throwable {
def containers = findAllContainers(true)
startContainers(containers, invocation)

def compose = findAllComposeContainers(true)
startComposeContainers(compose, invocation)

invocation.proceed()
}

void interceptCleanupSpecMethod(IMethodInvocation invocation) throws Throwable {
def containers = findAllContainers(true)
stopContainers(containers, invocation)

def compose = findAllComposeContainers(true)
stopComposeContainers(compose, invocation)

invocation.proceed()
}

@Override
void interceptSetupMethod(IMethodInvocation invocation) throws Throwable {
def containers = findAllContainers(false)
startContainers(containers, invocation)

def compose = findAllComposeContainers(false)
startComposeContainers(compose, invocation)

invocation.proceed()
}


@Override
void interceptCleanupMethod(IMethodInvocation invocation) throws Throwable {
def containers = findAllContainers(false)
stopContainers(containers, invocation)

def compose = findAllComposeContainers(false)
stopComposeContainers(compose, invocation)

invocation.proceed()
}

private List<FieldInfo> findAllContainers(boolean shared) {
spec.allFields.findAll { FieldInfo f ->
GenericContainer.isAssignableFrom(f.type) && f.shared == shared
}
}

private List<FieldInfo> findAllComposeContainers(boolean shared) {
spec.allFields.findAll { FieldInfo f ->
DockerComposeContainer.isAssignableFrom(f.type) && f.shared == shared
}
}

private static void startContainers(List<FieldInfo> containers, IMethodInvocation invocation) {
containers.each { FieldInfo f ->
GenericContainer container = readContainerFromField(f, invocation)
if(!container.isRunning()){
container.start()
}
}
}

private static void stopContainers(List<FieldInfo> containers, IMethodInvocation invocation) {
containers.each { FieldInfo f ->
GenericContainer container = readContainerFromField(f, invocation)
container.stop()
}
}

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

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


private static GenericContainer readContainerFromField(FieldInfo f, IMethodInvocation invocation) {
f.readValue(invocation.instance) as GenericContainer
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.testcontainers.spock

import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.HttpClientBuilder
import org.testcontainers.containers.DockerComposeContainer
import spock.lang.Specification

@Testcontainers
class ComposeContainerIT extends Specification {

DockerComposeContainer composeContainer = new DockerComposeContainer(
new File("src/test/resources/docker-compose.yml"))
.withExposedService("whoami_1", 80)

String host

int port

def setup() {
host = composeContainer.getServiceHost("whoami_1", 80)
port = composeContainer.getServicePort("whoami_1", 80)
}

def "running compose defined container is accessible on configured port"() {
given: "a http client"
def client = HttpClientBuilder.create().build()

when: "accessing web server"
def response = client.execute(new HttpGet("http://$host:$port"))

then: "docker container is running and returns http status code 200"
response.statusLine.statusCode == 200
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.testcontainers.spock

import org.testcontainers.containers.MySQLContainer
import spock.lang.Shared
import spock.lang.Specification

/**
* This test verifies, that setup and cleanup of containers works correctly.
* It's easily achieved using the <code>MySQLContainer</code>, since it will fail
* if the same image is running.
*
* @see <a href="https://github.com/testcontainers/testcontainers-spock/issues/19">Second container is started when stopping old container</a>
*/
@Testcontainers
class MySqlContainerIT extends Specification {

@Shared
MySQLContainer mySQLContainer = new MySQLContainer()

def "dummy test"() {
expect:
mySQLContainer.isRunning()
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.testcontainers.spock

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.testcontainers.containers.PostgreSQLContainer
import spock.lang.Shared
import spock.lang.Specification

import java.sql.ResultSet
import java.sql.Statement

@Testcontainers
class PostgresContainerIT extends Specification {

@Shared
PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer()
.withDatabaseName("foo")
.withUsername("foo")
.withPassword("secret")

def "waits until postgres accepts jdbc connections"() {

given: "a jdbc connection"
HikariConfig hikariConfig = new HikariConfig()
hikariConfig.setJdbcUrl(postgreSQLContainer.jdbcUrl)
hikariConfig.setUsername("foo")
hikariConfig.setPassword("secret")
HikariDataSource ds = new HikariDataSource(hikariConfig)

when: "querying the database"
Statement statement = ds.getConnection().createStatement()
statement.execute("SELECT 1")
ResultSet resultSet = statement.getResultSet()
resultSet.next()

then: "result is returned"
int resultSetInt = resultSet.getInt(1)
resultSetInt == 1

cleanup:
ds.close()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.testcontainers.spock

import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.HttpClientBuilder
import org.testcontainers.containers.DockerComposeContainer
import spock.lang.Shared
import spock.lang.Specification

@Testcontainers
class SharedComposeContainerIT extends Specification {

@Shared
DockerComposeContainer composeContainer = new DockerComposeContainer(
new File("src/test/resources/docker-compose.yml"))
.withExposedService("whoami_1", 80)

String host

int port

def setup() {
host = composeContainer.getServiceHost("whoami_1", 80)
port = composeContainer.getServicePort("whoami_1", 80)
}

def "running compose defined container is accessible on configured port"() {
given: "a http client"
def client = HttpClientBuilder.create().build()

when: "accessing web server"
def response = client.execute(new HttpGet("http://$host:$port"))

then: "docker container is running and returns http status code 200"
response.statusLine.statusCode == 200
}
}
Loading