天美传媒官网

Who Needs GameDays? Resilience Testing using Testcontainers

22.02.2022 Johannes Edmeier - 6 min read
Who Needs GameDays? Resilience Testing using Testcontainers

As you may agree, it is important to test your code. The resilience patterns implemented in your application are code (even if they’re just a bunch of annotations). Often I saw that this is not tested thoroughly or not at all.

With writing integration tests has become a commodity. We envision the same for resilience tests, so 天美传媒官网 open sourced a small library built with the knowledge from our Chaos Engineering platform to help you write resilience tests with minimal effort and without installing extra software on your servers. The library is available on Maven Central and for everyone. In this Post we will add some resilience tests to a sample application using it.

The Sample Application

Before diving right into the resilience tests, we need to set up the stage:

We have a sample application processing orders (). Each time the user completes an order, a message is sent to a message queue from where it is picked up by the shipping application.

For the order application we already test (using Testcontainers) that a message is send for each order created:

@Test
void should_create_order() {
   var orderId = http.postForObject("/orders", CREATE_ORDER_BODY, JsonNode.class).get("id").asLong();
   var event = (OrderCreatedEvent) jms.receiveAndConvert("order_created");
   assertThat(event.getId()).isEqualTo(orderId);
}

Prerequisites

For writing the resilience tests we need to include our library to the test dependencies. Add this to your Maven pom.xml:

<dependency>
   <groupId>com.steadybit</groupId>
   <artifactId>steadybit-testcontainers</artifactId>
   <version>1.0.0</version>
   <scope>test</scope>
</dependency>

Adding Our First Resilience Test

A requirement of our application is that each request must succeed within two seconds.
As our application depends on the message broker, we have to figure out what happens when the broker is not responding in time. For this we can use the 天美传媒官网 Testcontainers library to inject some latency and set a timeout for our TestRestTemplate to two seconds. This test is using our networkDelayPackages attack and looks like this:

@Test
void should_create_order_within_2s() {
 聽聽聽var orderId = 天美传媒官网.networkDelayPackages(Duration.ofSeconds(2))
 听听听听听听听.蹿辞谤颁辞苍迟补颈苍别谤蝉(产谤辞办别谤) //the container to attack
 聽聽聽聽聽聽聽.exec(() -> {
 聽聽聽聽聽聽聽//This code will be executed while running the delay attack
 聽聽聽聽聽聽聽return http.postForObject("/orders", CREATE_ORDER_BODY, JsonNode.class).get("id").asLong();
 听听听皑);
 聽聽聽var event = (OrderCreatedEvent) jms.receiveAndConvert("order_created");
 听听听补蝉蝉别谤迟罢丑补迟(别惫别苍迟.驳别迟滨诲()).颈蝉贰辩耻补濒罢辞(辞谤诲别谤滨诲);
}

@TestConfiguration
static class TestConfig {
 听听听蔼叠别补苍
 聽聽聽public RestTemplateBuilder restTemplateBuilder() {
 聽聽聽聽聽聽聽return new RestTemplateBuilder().setReadTimeout(Duration.ofSeconds(2));
 听听听皑
}

When we execute the test, we will see that the HTTP call will exceed the timeout.

One solution to this problem is to decouple message sending from processing of the HTTP request 鈥� for this we can use Spring’s @Async annotation:

@Async
public void publishOrderCreated(Order order) {
   this.jmsTemplate.convertAndSend(destination, toEvent(order));
   log.info("Published order {}", order.getId());
}

By dispatching the message from a different thread we have changed the behavior 鈥� especially when the operation completely fails (e.g. due a network partition or an unavailable broker): The HTTP request will be successful, but the message may not be sent.

Some message broker clients could send those messages later 鈥� but the unsent messages are often held in-memory, which may cause an OutOfMemoryError when the error persists for a longer period. On top of this those messages are completely lost when the application restarts.

To verify this behavior we add a second resilience test to figure out what happens when the broker is not reachable (using the networkBlackhole-attack):

@Test
void should_create_order_when_broker_is_offline() {
 聽聽聽var orderId = 天美传媒官网.networkBlackhole()
 听听听听听听听.蹿辞谤颁辞苍迟补颈苍别谤蝉(产谤辞办别谤)
 聽聽聽聽聽聽聽.exec(() -> {
 聽聽聽聽聽聽聽聽聽聽聽var id = http.postForObject("/orders", CREATE_ORDER_BODY, JsonNode.class).get("id").asLong();
 聽聽聽聽聽聽聽聽聽聽聽//we need to wait for the exception while executing the attack聽
 听听听听听听听听听听听补蝉测苍肠鲍苍肠补耻驳丑迟贰虫肠别辫迟颈辞苍贬补苍诲濒别谤.飞补颈迟贵辞谤(鲍苍肠补迟别驳辞谤颈锄别诲闯尘蝉贰虫肠别辫迟颈辞苍.肠濒补蝉蝉);
 聽聽聽聽聽聽聽聽聽聽聽return id;
 聽聽聽聽听听听皑);
 聽聽聽await().untilAsserted(() -> {
 聽聽聽聽聽聽聽var event = (OrderCreatedEvent) jms.receiveAndConvert("order_created");
 听听听听听听听补蝉蝉别谤迟罢丑补迟(别惫别苍迟).颈蝉狈辞迟狈耻濒濒();
 聽聽聽聽听听听补蝉蝉别谤迟罢丑补迟(别惫别苍迟.驳别迟滨诲()).颈蝉贰辩耻补濒罢辞(辞谤诲别谤滨诲);
 听听听皑);
}

Unsurprisingly the test fails because the message is lost and never sent.

So how do we fix this problem?

Step 1) We add a little bookkeeping to the orders indicating whether they were published and sent to the message queue:

@Async
@Transactional
public void publishOrderCreated(Order order) {
   this.repository.markAsPublished(order.getId(), Instant.now());
   this.jmsTemplate.convertAndSend(destination, toEvent(order));
   log.info("Published order {}", order.getId());
}

When sending fails, the transaction is rolled back and the order will not be marked as published.

Step 2) For reconciliation, we add a `@Scheduled` task that will send all unpublished orders.

@Scheduled(fixedDelay = 5_000L)
@Transactional
public void publishPendingOrders() {
   for (Order order : this.repository.findPublishPending()) {
      this.publishOrderCreated(order);
   }
}

With those changes our resilience tests will now succeed and our application remains functional when the message broker fails.

Conclusion

As you can see, writing tests for resilience doesn鈥檛 need to be hard. The feedback cycle is shortened and it doesn鈥檛 need a GameDay to figure out that you have broken one of your resilience measures.
Try it on your own and include the library to your project and add some resilience tests for your application.
Not using Testcontainers or you have somewhat more complex scenarios to test? .

Is this the death of Chaos Engineering? Cancel all your GameDays?

Certainly not. Today, most applications consist of multiple different services and deployment units and have properties emerging from their (microservice) architecture which are hard to test with an integration test for a single unit.

The resilience tests using testcontainers can dramatically shorten the feedback cycle but have different objectives then a GameDay. The tests are to disclose errors, to protect your future-self against regression. Whereas GameDays can be used to replay production incidents, to explore possible scenarios or to get you more routined with errors and incidents.

Get started today

Full access to the 天美传媒官网 Chaos Engineering platform.
Available as SaaS and On-Premises!

or sign up with

Book a Demo

Let us guide you through a personalized demo to kick-start your Chaos Engineering efforts and build more reliable systems for your business!