Toxiproxy - Spring Boot setup
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.