Testing Message-Driven Application Business Logic without Message Broker Dependencies

Sugumar Panneerselvam
4 min readApr 14, 2024

--

Photo by amirali mirhashemian on Unsplash

Introduction

In continuation of our previous article, where we explored Spring Cloud Stream for creating message broker-agnostic publishers and consumers, we now getting into the realm of testing the business logic within these applications. While much of the message broker setup, publishing, and subscribing functionalities are well-tested by respective providers, ensuring the correctness of the business logic remains most important. However, testing this logic without a message broker setup can be a challenge. This is where Spring Integration Test Binder comes into play.

In this article, we will explore how Spring Integration Test Binder empowers developers to test the business logic of message-driven Spring applications without the need for a fully-fledged message broker environment.

If you need to test the application with a particular broker (like RabbitMQ or Kafka in our case), you can use Testcontainers.

Let’s dive into the world of testing message-driven applications with Spring Integration Test Binder.

Understanding Spring Integration Test Binder

Having unit tests to check the business logic independently is smart. Yet, it’s also valuable to add integration tests to see how your app behaves in a Spring Cloud Stream setup. Spring Cloud Stream provides a specific built-in tool, called a test binder, for building integration tests. It focuses on testing your business logic rather than the technical parts.

Spring Cloud Stream provides a test binder that enables you to test your application components without needing a real message broker. This test binder serves as a connection between unit and integration testing, leveraging the Spring Integration framework to simulate a message broker within the JVM. Essentially, it offers the benefits of a real binder without the need for network communication.

Refer https://docs.spring.io/spring-cloud-stream/reference/spring-cloud-stream/spring_integration_test_binder.html

Enabling Spring Integration Test Binder

To enable Spring Integration Test Binder, all you need is to add it as a dependency in your project. Below are the Maven POM entries required:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-binder</artifactId>
<scope>test</scope>
</dependency>

Implementing Test Binder for a Financial Use Case

Once you have added the dependency, you can configure your message processing functions using Spring Cloud Stream annotations. Here’s an example of configuring functions for a financial use case:

  1. Late Fee Calculation: Calculate a late fee for a transaction based on a predefined percentage of the transaction amount. This method ensures that overdue transactions incur an additional charge, helping to incentivize timely payments and mitigate financial risks.
  2. Transaction Due Date Evaluation: Determine if a transaction is overdue by comparing its timestamp with the current date. This method helps identify transactions that have exceeded their due date, allowing businesses to prioritize follow-up actions such as reminders or penalty assessments.
@EnableAutoConfiguration
public static class SampleFunctionConfiguration {

@Bean
public Function<Transaction, Transaction> addLateFee() {
return transaction -> {
// Assuming late fee is 10% of the transaction amount
double lateFeePercentage = 0.10;
double lateFeeAmount = transaction.getAmount() * lateFeePercentage;

// Creating a new transaction with late fee added
Transaction transactionWithLateFee = new Transaction(
transaction.getId(),
transaction.getCurrency(),
transaction.getAmount() + lateFeeAmount
);
return transactionWithLateFee;
};
}

@Bean
public Function<Transaction, Boolean> isTransactionOverdue() {
return transaction -> {
// Assume a transaction is considered overdue if it's more than 30 days old
long daysSinceTransaction = // Logic to calculate days since the transaction
return daysSinceTransaction > 30;
};
}
}
  • We define a Function<Transaction, Transaction> bean named addLateFee, which represents the logic for adding a late fee to a transaction.
  • Inside the function, we calculate the late fee amount, assuming it to be 10% of the transaction amount.
  • We then create a new Transaction object with the late fee added to the original transaction amount and return it.
  • We define a new Function<Transaction, Boolean> bean named isTransactionOverdue, which represents the logic for determining if a transaction is overdue.
  • Inside the function, we can implement any logic to calculate the days since the transaction and return true if it's overdue (more than 30 days old) or false otherwise.

Writing Integration Tests

With Spring Integration Test Binder, you can write integration tests to validate the behavior of your message processing functions. Here’s an example of a test case:

import org.junit.jupiter.api.Test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.context.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import static org.assertj.core.api.Assertions.assertThat;

public class SampleFunctionConfigurationTest {

@Test
public void testAddLateFee() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(SampleFunctionConfiguration.class))
.run("--spring.cloud.function.definition=addLateFee")) {

InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context.getBean(OutputDestination.class);

// Prepare a test transaction
Transaction transaction = new Transaction("123456789", "USD", 1000.00);
Message<Transaction> inputMessage = MessageBuilder.withPayload(transaction).build();

// Send the test transaction to the input channel
inputDestination.send(inputMessage, "addLateFee-in-0");

// Receive the processed transaction from the output channel
Message<byte[]> outputMessage = outputDestination.receive(0, "addLateFee-out-0");

// Validate the received message
assertThat(outputMessage).isNotNull();
Transaction processedTransaction = new Transaction(outputMessage.getPayload());
assertThat(processedTransaction.getAmount()).isEqualTo(1100.00); // Assuming 10% late fee added
}
}
}

In this test case:

  • We use SpringApplicationBuilder to configure the test environment with the SampleFunctionConfiguration class, which contains the financial use case functions.
  • We use TestChannelBinderConfiguration.getCompleteConfiguration to configure the test environment with the necessary binder channels.
  • We send a test transaction to the addLateFee function input channel.
  • We receive the processed transaction from the addLateFee function output channel and validate the late fee addition.
  • We also test the isTransactionOverdue function output channel to ensure no overdue transactions are detected in this test.

Conclusion

Spring Integration Test Binder provides a powerful testing solution for message-driven Spring applications, allowing developers to validate the business logic of their applications in isolation. By simulating message exchange scenarios without the need for a real message broker environment, Spring Integration Test Binder enables reliable and efficient testing practices, ensuring the correctness and robustness of message processing components.

Incorporating Spring Integration Test Binder into your testing strategy can enhance the quality and reliability of your message-driven Spring applications, enabling you to deliver high-quality software with confidence.

Happy Integration!

--

--