Toxiproxy - Spring Boot setup

Page content

This article is the first in a series on Toxiproxy. It covers how to set up Toxiproxy and verify that it works in a Java environment using Spring Boot.

Setting Up a Project

This article assumes you have a working installation of Docker.

Clone the project: toxiproxy-spring-boot-starter.

Overview

In the project you will find a very simple endpoint that can return the time in a json structure to a GET request to /:

 1@SpringBootApplication
 2@RestController
 3public class App {
 4  public static void main(String[] args) {
 5    SpringApplication.run(App.class, args);
 6  }
 7
 8  @GetMapping
 9  Time now() {
10    return new Time(LocalDateTime.now().toString());
11  }
12
13  static class Time {
14    private String time;
15
16    public Time(String time) {
17      this.time = time;
18    }
19
20    public String getTime() {
21      return time;
22    }
23  }
24}

Now, what we want to test is:

  • that the application works when called by a client (a normal Spring Boot integration test where RestTemplate acts as the client), a sanity test

  • that a client, the RestTemplate configured with a 5 second read timeout, exhibits these expected behaviors:

    • given the route to the application is proxied, when the client call the server via the proxy, it should not see any difference from when calling the server directly
    • given there is a 4 second latency in the response from the server, when the client calls the server via the proxy, it should still work
    • given there is a 6 second latency in the response from the server, when the client calls the server via the proxy, it should get a SocketTimeoutException

The sanity test

The first test case is standard Spring boot. Worth noticing is that it uses the port injected via @LocalServerPort:

 1@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
 2public class AppTest {
 3
 4  @LocalServerPort private int port;
 5
 6  @TestConfiguration
 7  static class TestConfig {
 8    @Bean
 9    RestTemplateBuilder restTemplateBuilder() {
10      return new RestTemplateBuilder().setReadTimeout(Duration.of(5, ChronoUnit.SECONDS));
11    }
12  }
13
14  @Autowired private TestRestTemplate restTemplate;
15
16  @Test
17  public void worksNoProxy() {
18    ResponseEntity<String> stringResponseEntity =
19        this.restTemplate.getForEntity("http://localhost:" + port + "/", String.class);
20    assertEquals(HttpStatus.OK, stringResponseEntity.getStatusCode());
21  }
22}
graph TD;
    Client["Client calling server on port @LocalServerPort"]-->S["Server listening on port @LocalServerPort"];

Introducing Toxiproxy

The next test, proxying the server, introduces these changes to the AppTest class:

  • Annotate the class with @Testcontainers - leveraging Testcontainers to manage starting and stopping the Toxiproxy docker container,
  • Introduce the fields:
1  private int proxyPort = 20001;
2
3  @Container
4  private GenericContainer<?> toxiproxy =
5      new GenericContainer<>(
6              DockerImageName.parse("ghcr.io/shopify/toxiproxy:2.3.0")
7                  .asCompatibleSubstituteFor("shopify/toxiproxy"))
8          .withExposedPorts(8474, proxyPort);

Port 8474 is for the Toxiproxy HTTP endpoint. It is used by the ToxiproxyClient in the tests to control the behavior of Toxiproxy.

The proxyPort is the port the client will use when calling the application. Now for the test itself:

 1  @Test
 2  public void proxyOnlyWorks() throws IOException {
 3    ToxiproxyClient toxiproxyClient =
 4        new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getFirstMappedPort());
 5
 6    InetAddress hostRunningTheAppEndpoint = InetAddress.getLocalHost();
 7
 8    toxiproxyClient.createProxy(
 9        "my-proxy",
10        "0.0.0.0:" + proxyPort,
11        hostRunningTheAppEndpoint.getHostAddress() + ":" + port);
12
13    ResponseEntity<String> stringResponseEntity =
14        this.restTemplate.getForEntity(
15            "http://localhost:" + toxiproxy.getMappedPort(proxyPort) + "/", String.class);
16    assertEquals(HttpStatus.OK, stringResponseEntity.getStatusCode());
17  }

The tests sets up the Toxiproxy to listen on the proxyPort and forward traffic to the host running the test on the port injected by @LocalServerPort. So it just proxies the traffic.

graph TD;
    Client["Client calling proxy on port proxyPort (20001)"]-->Proxy[Proxy listening on proxyPort]
    Proxy["Proxy forward to server host 'hostRunningTheAppEndpoint.getHostAddress()':@LocalServerPort"]-->S["Server listening on port @LocalServerPort"];;

The next two tests are very similar, except they introduce downstream latency:

 1  @Test
 2  public void proxyWithLatency() throws IOException {
 3    ToxiproxyClient client =
 4        new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getFirstMappedPort());
 5
 6    InetAddress hostRunningTheAppEndpoint = InetAddress.getLocalHost();
 7
 8    Proxy proxy =
 9        client.createProxy(
10            "my-proxy", "0.0.0.0:" + proxyPort, hostRunningTheAppEndpoint.getHostAddress() + ":" + port);
11    proxy.toxics().latency("latency", ToxicDirection.DOWNSTREAM, 4_000);
12
13    ResponseEntity<String> stringResponseEntity =
14        this.restTemplate.getForEntity(
15            "http://localhost:" + toxiproxy.getMappedPort(proxyPort) + "/", String.class);
16    assertEquals(HttpStatus.OK, stringResponseEntity.getStatusCode());
17  }
18
19  @Test
20  public void proxyWithLatencyTimeout() throws IOException {
21    ToxiproxyClient client =
22        new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getFirstMappedPort());
23
24    InetAddress hostRunningTheAppEndpoint = InetAddress.getLocalHost();
25
26    Proxy proxy =
27        client.createProxy(
28            "my-proxy", "0.0.0.0:" + proxyPort, hostRunningTheAppEndpoint.getHostAddress() + ":" + port);
29    proxy.toxics().latency("latency", ToxicDirection.DOWNSTREAM, 6_000);
30
31    ResourceAccessException resourceAccessException =
32        assertThrows(
33            ResourceAccessException.class,
34            () ->
35                this.restTemplate.getForEntity(
36                    "http://localhost:" + toxiproxy.getMappedPort(proxyPort) + "/", String.class));
37
38    assertEquals(SocketTimeoutException.class, resourceAccessException.getRootCause().getClass());
39  }

Conclusion

This article explores a basis but useful setup of Toxiproxy. In the next article I’ll go more into depth on how to use it for resilience testing.