Testcontainers – Unleash Your (Unit) Tests Using Docker (2/3)

In a previous post, we learned what Testcontainers is; now you’ll see some examples of its use.

Use Case: Integration Tests with Database

Imagine, your production system makes use of a Postgres database. Without Testcontainers, you would typically set up an in-memory database like H2 to back your persistence service. If you’re lucky, H2 supports all the features you need, if not, you’ll have to develop expensive workarounds.

With Testcontainers, you can easily setup a real Postgres database within a Docker container, giving you more confidence that your code behaves the same in production as in the tests. Furthermore, in contrast to manually maintaining a database installation in a VM for example, you don’t need to worry about remnants of test data of previous tests. Instead, you’ll always start from a known state.

Test setup without (left) and with Testcontainers (right)

Starting a Postgres instance becomes as simple as this:

public class SimplePostgreSQLTest extends AbstractContainerDatabaseTest {

    @Test
    public void testSimple() throws SQLException {
        try (PostgreSQLContainer postgres = new PostgreSQLContainer<&gt;()) {
            postgres.start();

            ResultSet resultSet = performQuery(postgres, "SELECT 1");
            int resultSetInt = resultSet.getInt(1);
            assertEquals("A basic SELECT query succeeds", 1, resultSetInt);
        }
    }
}

Another use case might be testing for database independence by running the same tests against multiple database that are to be supported by the SUT. This is facilitated by the modules for several (non-)SQL database that are already provided:

Databases supported out-of-the-box

Use Case: Cross-Browser Testing

Testcontainers also offers support for testing web interfaces using the industry standard Selenium framework. You should read this great article (in German) if you want to familiarize yourself with Selenium.

As of today, there are two browsers supported out-of-the-box: Firefox and Chrome. This is due to the restriction of Docker that it (usually) virtualizes Linux-based images. The modules for these browsers offer a RemoteWebDriver instance that can be used like any local WebDriver instance.

public class BaseWebDriverContainerTest {

    protected void doSimpleWebdriverTest(BrowserWebDriverContainer rule) {
        RemoteWebDriver driver = setupDriverFromRule(rule);
        
        driver.get("http://www.google.com");
        WebElement search = driver.findElement(By.name("q"));
        search.sendKeys("testcontainers");
        search.submit();

        List<WebElement&gt; results = new WebDriverWait(driver, 15)
                .until(ExpectedConditions.visibilityOfAllElementsLocatedBy(By.cssSelector("#search h3")));

        assertTrue("the word 'testcontainers' appears in search results",
                results.stream()
                        .anyMatch(el -&gt; el.getText().contains("testcontainers")));
    }

}

A huge advantage of this approach is the fact that these tests run on headless containers, so there is zero chance of manual user interactions interfering with the UI tests. What’s more, a screen recording – of either all or just failing runs, which can be configured – is just one line of code away. A short sample of the above code in action can be seen here:

Recording of a simple Selenium test: Performing a Google search

Under the hood, the VNC stream provided by the container is recorded, which implies that during test development or long-running tests, it could also be observed manually using any VNC client.

As these browser containers start up fairly quickly, it is feasible and advisable to start a new one for each test. This way, you can also be sure to start off from a “clean” browser instance, i.e. a known state. Furthermore, this makes cross-browser testing for Chrome and Firefox quite effortless, by simply running the same tests against both containerized browsers.

Test setup for cross-browser testing

Use Case: Resilience Testing with Toxiproxy

Automated integration tests usually don’t incorporate chaos in the form of unreliable network connections, as they exist in the real world. Rather, these tests run on perfectly working localhost connections. In a world increasingly embracing microservices and their interdependence, this borders on negligence. A sneaky tool to test your application under adverse networking conditions is Toxiproxy. With Testcontainers, it is simple to wire up your (not necessarily containerized) applications and configure so-called “toxics” for their TCP connections such as:

  • Latency and jitter (in ms)
  • Bandwidth (in KB/s)
  • Slow close (in ms): Delays the TCP socket from closing until delay has elapsed.
  • Timeout (in ms): Stops all data from getting through and closes the connection after timeout.
  • Slicer: Slices TCP data up into small bits, optionally adding a delay between each sliced „packet“.
  • Data limit (in bytes): Closes connection when transmitted data exceeded limit.
Setup for resilience testing using Toxiproxy

The above example (with another $System omitted for brevity) looks like this in code:

public class ToxiproxyTest {

    // Create a common docker network so that containers can communicate
    @Rule
    public Network network = Network.newNetwork();

    // the target container - this could be anything
    @Rule
    public GenericContainer redis = new GenericContainer("redis:5.0.4")
        .withExposedPorts(6379)
        .withNetwork(network);

    @Rule
    public ToxiproxyContainer toxiproxy = new ToxiproxyContainer()
        .withNetwork(network);

    @Test
    public void testConnectionCut() {
        final ToxiproxyContainer.ContainerProxy proxy = toxiproxy.getProxy(redis, 6379);
        final Jedis jedis = new Jedis(proxy.getContainerIpAddress(), proxy.getProxyPort());
        jedis.set("somekey", "somevalue");

        assertEquals("access to the container works OK before cutting the connection", "somevalue", jedis.get("somekey"));

        proxy.setConnectionCut(true);

        assertThrows("calls fail when the connection is cut",
            JedisConnectionException.class, () -&gt; {
                jedis.get("somekey");
            });

        proxy.setConnectionCut(false);

        assertEquals("access to the container works OK after re-establishing the connection", "somevalue", jedis.get("somekey"));
    }
}

Need Anything Else?

As of now, there are several other modules for further use cases already available:

If you don’t find a module for your use case, you can either start your containerized application explicitly and implement the communication with it directly or package this knowledge into your own module to make it reusable. The building blocks like wait strategies (waiting for a containerized application to become available, e.g. respondings to HTTP connections on a port or waiting for a certain log output) are already there.

To learn how to integrate Testcontainers with container-based CI systems, take a look at this follow-up post.

One thought on “Testcontainers – Unleash Your (Unit) Tests Using Docker (2/3)”

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.

%d Bloggern gefällt das: