Skip to content

Toxiproxy Module

Testcontainers module for Shopify's Toxiproxy. This TCP proxy can be used to simulate network failure conditions.

You can simulate network failures:

  • between Java code and containers, ideal for testing resilience features of client code
  • between containers, for testing resilience and emergent behaviour of multi-container systems
  • if desired, between Java code/containers and external resources (non-Dockerized!), for scenarios where not all dependencies can be/have been dockerized

Testcontainers Toxiproxy support allows resilience features to be easily verified as part of isolated dev/CI testing. This allows earlier testing of resilience features, and broader sets of failure conditions to be covered.

Usage example

A Toxiproxy container can be placed in between test code and a container, or in between containers. In either scenario, it is necessary to create a ToxiproxyContainer instance on the same Docker network, as follows:

// 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)
    .withNetworkAliases("redis");

// Toxiproxy container, which will be used as a TCP proxy
@Rule
public ToxiproxyContainer toxiproxy = new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.5.0")
    .withNetwork(network);

Next, it is necessary to instruct Toxiproxy to start proxying connections. Each ToxiproxyContainer can proxy to many target containers if necessary.

We do this as follows:

final ToxiproxyClient toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort());
final Proxy proxy = toxiproxyClient.createProxy("redis", "0.0.0.0:8666", "redis:6379");

To establish a connection from the test code (on the host machine) to the target container via Toxiproxy, we obtain Toxiproxy's proxy host IP and port:

final String ipAddressViaToxiproxy = toxiproxy.getHost();
final int portViaToxiproxy = toxiproxy.getMappedPort(8666);

Code under test should connect to this proxied host IP and port.

Note

Currently, ToxiProxyContainer will reserve 31 ports, starting at 8666.

Other containers should connect to this proxied host and port.

Having done this, it is possible to trigger failure conditions ('Toxics') through the proxy.toxics() object:

  • bandwidth - Limit a connection to a maximum number of kilobytes per second.
  • latency - Add a delay to all data going through the proxy. The delay is equal to latency +/- jitter.
  • slicer - Slices TCP data up into small bits, optionally adding a delay between each sliced "packet".
  • slowClose - Delay the TCP socket from closing until delay milliseconds has elapsed.
  • timeout - Stops all data from getting through, and closes the connection after timeout. If timeout is 0, the connection won't close, and data will be delayed until the toxic is removed.
  • limitData - Closes connection when transmitted data exceeded limit.

Please see the Toxiproxy documentation for full details on the available Toxics.

As one example, we can introduce latency and random jitter to proxied connections as follows:

proxy.toxics()
    .latency("latency", ToxicDirection.DOWNSTREAM, 1_100)
    .setJitter(100);
// from now on the connection latency should be from 1000-1200 ms.

Additionally we can disable the proxy to simulate a complete interruption to the network connection:

proxy.toxics().bandwidth("CUT_CONNECTION_DOWNSTREAM", ToxicDirection.DOWNSTREAM, 0);
proxy.toxics().bandwidth("CUT_CONNECTION_UPSTREAM", ToxicDirection.UPSTREAM, 0);

// for example, expect failure when the connection is cut
assertThat(
    catchThrowable(() -> {
        jedis.get("somekey");
    })
)
    .as("calls fail when the connection is cut")
    .isInstanceOf(JedisConnectionException.class);

proxy.toxics().get("CUT_CONNECTION_DOWNSTREAM").remove();
proxy.toxics().get("CUT_CONNECTION_UPSTREAM").remove();

// and with the connection re-established, expect success
assertThat(jedis.get("somekey"))
    .as("access to the container works OK after re-establishing the connection")
    .isEqualTo("somevalue");

Adding this module to your project dependencies

Add the following dependency to your pom.xml/build.gradle file:

testImplementation "org.testcontainers:toxiproxy:1.19.7"
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>toxiproxy</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>

Acknowledgements

This module was inspired by a hotels.com blog post.

toxiproxy-java is used to help control failure conditions.