fabzo / kraken

Docker integration testing support for Java
Apache License 2.0
6 stars 0 forks source link
docker integration-testing java kubernetes

Kraken

Kraken allows you to create a test environment based on docker containers.

Use MySQL instead of H2, Kafka instead of embedded or mocked versions or spin up a Redis container.

A few notes:

Dependencies

Gradle:

maven { url 'https://jitpack.io' }
testCompile 'com.github.fabzo:kraken:0.4'

Maven:

<repository>
  <id>jitpack.io</id>
  <url>https://jitpack.io</url>
</repository>

<dependency>
    <groupId>com.github.fabzo</groupId>
    <artifactId>kraken</artifactId>
    <version>0.4</version>
</dependency>

Example usage

Creating a new test environment is done by implementing a new EnvironmentModule and registering LifecycleHandlers as well as InfrastructureComponents. The following example registers the DockerLifecycleHandler as well as a DockerComponent.

public class MyTestingModule extends EnvironmentModule {
    @Override
    public void configure() {
        register(DockerLifecycleHandler.withConfig(
            DockerConfiguration.create()
                .withDockerSocket(DockerConfiguration.DOCKER_HOST_UNIX)));

        register(DockerComponent.create()
            .withName("mariadb")
            .withImage("mariadb", "latest")
            .withForcePull()
            .withFollowLogs()
            .withPortBinding("db", 3306)
            .withEnv("MYSQL_DATABASE", "testdb")
            .withEnv("MYSQL_ALLOW_EMPTY_PASSWORD", "yes")
            .withWait(new MySQLWait("testdb","db", Duration.ofSeconds(60))));
    }
}

Using Kraken we can now create a new Environment from the previously defined module, start and stop it:

final Environment environment = Kraken.createEnvironment(new MyTestingModule());

try {
    environment.start();
} catch (final Exception e) {
    e.printStackTrace();
} finally {
    environment.stop();
}

And Kraken will spin up a new mariadb:

12:14:16.424 [main] INFO  fabzo.kraken.Kraken - Configuring module fabzo.kraken.WaitTest$1
12:14:16.482 [main] INFO  fabzo.kraken.Environment - Using environment salt aggkkcfp
12:14:16.491 [main] INFO  fabzo.kraken.Environment - Starting all components of fabzo.kraken.WaitTest$1
12:14:16.495 [main] INFO  fabzo.kraken.Environment - Starting mariadb using docker handler
12:14:16.937 [main] INFO  f.k.handler.docker.DockerHandler - Pulling image mariadb:latest
12:14:19.959 [main] INFO  f.k.handler.docker.DockerHandler - Starting mariadb
12:14:20.562 [main] INFO  f.k.handler.AbstractLifecycleHandler - Waiting for mariadb using MySQLWait{username=root, driver=mysql, database=testdb, portName=db, atMost=PT1M, connectionUrl=None}
12:14:20.564 [main] INFO  fabzo.kraken.wait.DatabaseWait - Waiting for database to become available for up to PT1M
12:14:20.564 [main] INFO  fabzo.kraken.wait.DatabaseWait - Connection URL is jdbc:mysql://192.168.99.1:57294/testdb?user=root&password=&useUnicode=true&characterEncoding=utf8&useSSL=false&nullNamePatternMatchesAll=true
12:14:21.086 [dockerjava-jaxrs-async-1] INFO  f.k.h.d.c.LogContainerResultCallback - [mariadb] STDOUT: Initializing database
...

Example for local and kubernetes usage

The following test module uses some environment variable to detect if it is running in kubernetes or on a simple host with docker. In case it runs in kubernetes it configures a KubernetesLifecycleHandler with the ability to run DockerComponents (it is plannend to have more sophisticated KubernetesComponents).

public class MyTestingModule extends EnvironmentModule {
    public static final String MARIA_DB = "mariadb";

    public void configure() {
        val runningInKubernetes = System.getenv("SOME_ENV_VARIABLE") != null;

        if (runningInKubernetes) {
            register(KubernetesLifecycleHandler.withConfig(
                    KubernetesConfiguration.create()
                            .withRunDockerComponents(true)
                            .withExposeService(false)
                            .withNamespace("default")));
        } else {
            register(DockerLifecycleHandler.withConfig(
                    DockerConfiguration.create()
                            .withDockerSocket(DockerLifecycleHandler.DOCKER_HOST_UNIX)));
        }

        register(new DockerComponent()
                .withName(MARIA_DB)
                .withImage("mariadb", "latest")
                .withForcePull()
                .withPortBinding("db", 3306)
                .withEnv("MYSQL_DATABASE", "testdb")
                .withEnv("MYSQL_ALLOW_EMPTY_PASSWORD", "yes")
                .withWait(new MySQLWait("testdb","db", Duration.ofSeconds(60))));
    }
}

In order to ensure that your environment is always available when running integration tests you can create an abstract class that takes care of starting the environment. It will create the environment, start it and retrieve the mariadb ip and port to set as system propertries.

@Ignore("Abstract Base Test Class")
@RunWith(SpringJUnit4ClassRunner.class)
public class AbstractIntegrationTests {
    private static final String MYSQL_PORT = "mysqlport";
    private static final String MYSQL_IP = "mysqlip";

    private static Environment environment;

    @BeforeClass
    public static void beforeClass() {
        if (environment == null) {
            environment = Kraken.createEnvironment(new MyTestingModule());
            environment.start();

            final EnvironmentContext ctx = environment.context();

            final Option<String> mariaDBPort = ctx
                    .port(MARIA_DB, "db")
                    .getOrElseThrow(() -> new IllegalStateException("Unable to retrieve maria db port"));
            System.setProperty(MYSQL_PORT, mariaDBPort.toString());

            // We are not getting an Option<String> here since the ip()
            // method automatically falls back to the public facing ip or
            // localhost.
            final String mariaDBIP = environment.context().ip(MARIA_DB);
            System.setProperty(MYSQL_IP, mariaDBIP);
        }
    }

    private static boolean isLocalDatabaseRunning() {
        return System.getProperty(MYSQL_PORT) != null;
    }
}

Property replacement

The Docker and Kubernetes lifecycle handlers use the EnvironmentContext to store information like ports and IPs of started components. These can in turn be used when configuring the environment variables on components.

Inside the EnvironmentModule:

Outside the environment on the EnvironmentContext

Test Dependencies

The integration tests start and stop docker containers and as such require docker to be installed. The kubernetes part requires a local installation of minikube which will be used to start pods and services.

tl;dr: Install docker and minikube (which also requires VirtualBox)