Testcontainers

java

Recently I’ve found a very interesting project which allows to spin up docker containers for test purposes. Writing tests checking integration with external services is not an easy task. With testcontainers library it gets simpler because you can have external service up and running just for your test in a couple of lines of code.

Why

Couple times in the past I’ve faced the problem which can be summarized in the following statement:

Should I write my integration tests against stubbed/mocked service or maybe I should verify it against a live application.

Sometimes the integration is a critical part from an application perspective and you’d just sleep better if you know that it’s working just fine with live service not only with mocks. In the past writing tests like this required a lot of infrastructure configuration to have service prepared for testing. But with docker and testcontainers project things are much simpler now. Docker simplifies a lot of things in the development process but when you can spin up the container with required service directly from tests exactly when it’s needed that’s a huge simplification of the development process.

Example

I’m not going to do a lot of explaining here as the project have some solid and easy to understand and follow documentation. Instead, let’s write some tests and see if it works ;)

I’ll do something really simple so it’s easy to understand and will show the potential of this tool. Assume we have to verify some communication with Redis.

First of all, let’s declare a couple of dependencies that will be required in this demo:

dependencies {
	implementation "redis.clients:jedis:3.0.1"
	implementation "ch.qos.logback:logback-classic:1.2.3"
	testImplementation "org.codehaus.groovy:groovy-all:2.5.7"
	testImplementation "org.spockframework:spock-core:1.3-groovy-2.5"
	testImplementation "org.testcontainers:spock:1.11.4"
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import redis.clients.jedis.Jedis;

import java.util.Optional;

public class KeyValueStorage {
    private final Jedis jedis;

    public KeyValueStorage(String host, int port) {
        jedis = new Jedis(host, port);
    }

    public void set(String key, String value) {
        jedis.set(key, value);
    }

    public Optional<String> get(String key) {
        return Optional.ofNullable(jedis.get(key));
    }
}

Now we have our class to check if the integration works fine and we can easily write tests to verify it’s behavior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.output.Slf4jLogConsumer
import org.testcontainers.containers.wait.strategy.Wait
import org.testcontainers.spock.Testcontainers
import spock.lang.Specification
import spock.lang.Subject

@Testcontainers
class TestContainersTest extends Specification {
    private static final Logger log = LoggerFactory.getLogger(TestContainersTest)

    private GenericContainer redis = new GenericContainer<>("redis:5.0.3-alpine")
            .withLogConsumer(new Slf4jLogConsumer(log))
            .withExposedPorts(6379)
            .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1))

    @Subject
    private KeyValueStorage keyValueStorage

    void setup() {
        keyValueStorage = new KeyValueStorage(
                redis.containerIpAddress,
                redis.firstMappedPort)
    }

    def "puts value to storage"() {
        when:
        keyValueStorage.set("asd", "zxc")

        then:
        keyValueStorage.get("asd") == Optional.of("zxc")
    }

    def "gets value from storage"() {
        when:
        keyValueStorage.set("asd", "zxc")

        then:
        keyValueStorage.get("asd") == Optional.of("zxc")
        keyValueStorage.get("missing key") == Optional.empty()
    }
}

Once you start reading the code you’ll notice annotation org.testcontainers.spock.Testcontainers (line 10) (spock extension) and is responsible for container lifecycle management.

Next, we declare and initialize container itself using GenericContainer (line 14). Note that there is a couple of ready to use containers provided by authors of the library).

We can easily get exposed port and IP address of the service running in the container in the setup block (lines  24 and 25). And with that, we can write tests against live Redis instance (lines  28 and 36) without doing and docker stuff by hand :)

Documentation

As I’ve mentioned earlier the documentation is pretty good and here are a couple of pages that I think will be useful as a reference to the things you might want to do or check before deciding to use it:

Summary

I’m really glad I’ve stumbled upon this project as I’ve already identified a couple of places where I could have used it instead of writing some bizarre mocks or stubs to provide basic functionality that is required to run some process. Next time I’ll be facing similar dilemma I’ll consider this library and most likely try it and see it in the wild :)

See Also

If you've enjoyed or found this post useful you might also like:

18 Jul 2019 #java #testing #spock #docker