Mocking AmazonSQS in Unit Tests
Unit tests should be fast, isolated, and deterministic. When your Java application interacts with AWS SQS, you want to avoid hitting the real AWS environment during unit tests to keep them fast and reliable. Instead of invoking real network calls, you mock the AWS SDK v2 SqsClient to simulate SQS behavior. This allows you to verify that your code builds the correct requests and handles responses properly, without any external dependencies. Let us delve into understanding how to mock AmazonSQS in Java unit tests effectively.
1. What Is AmazonSQS?
AmazonSQS is a fully managed message queuing service that enables asynchronous communication between distributed application components. AWS SDK for Java offers two generations of clients:
AmazonSQS: The AWS SDK v1 client interface, which is an interface and easier to mock.SqsClient: The AWS SDK v2 client, which is immutable, uses builders for requests and responses, and is a final class.
1.1 Mockito for Mocking AmazonSQS
Mockito is a popular Java mocking framework commonly used to create mock objects for unit testing. Since the AWS SDK v1 AmazonSQS is an interface, Mockito can easily mock it, allowing developers to simulate and verify interactions with SQS without making actual network calls. This makes unit testing faster and more reliable by isolating the logic that interacts with SQS.
2. Code Example
2.1 Dependencies
The following Maven dependencies are required to use the AWS SDK for SQS along with JUnit 5 and Mockito for writing and running unit tests.
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
<version>stable__jar__version</version> <!-- Use latest stable version -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>stable__jar__version</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>stable__jar__version</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>stable__jar__version</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>stable__jar__version</version>
<scope>test</scope>
</dependency>
</dependencies>
2.2 Create Publisher and Consumer Classes
We’ll create two tiny classes that wrap SQS operations, which we’ll unit test by mocking SqsClient.
2.2.1 Publisher Class
The following code demonstrates a basic implementation of an Amazon SQS publisher using the AWS SDK for Java v2.
package example.sqs;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
import software.amazon.awssdk.services.sqs.model.SendMessageResponse;
import java.util.Objects;
public class SqsOrderPublisher {
private final SqsClient sqsClient;
private final String queueUrl;
public SqsOrderPublisher(SqsClient sqsClient, String queueUrl) {
this.sqsClient = Objects.requireNonNull(sqsClient);
this.queueUrl = Objects.requireNonNull(queueUrl);
}
public String publish(String orderJson) {
SendMessageRequest request = SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody(orderJson)
.delaySeconds(0)
.build();
SendMessageResponse response = sqsClient.sendMessage(request);
System.out.println("Published messageId=" + response.messageId());
return response.messageId();
}
}
The SqsOrderPublisher class provides a straightforward implementation for sending messages to an Amazon SQS queue using the AWS SDK v2. It requires an instance of SqsClient and a queue URL, both validated to be non-null in the constructor. The publish method builds a SendMessageRequest with the specified queue URL, message body containing the order JSON, and no delay. It then calls sqsClient.sendMessage to send the message, prints the returned message ID for confirmation, and returns this ID to the caller. This design encapsulates the message publishing logic cleanly, making it easy to use and test.
2.2.2 Consumer Class
The following code illustrates a simple Amazon SQS consumer implementation using the AWS SDK for Java v2 that polls messages from a queue, processes them with a provided handler, and deletes each message after successful processing.
package example.sqs;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.DeleteMessageRequest;
import software.amazon.awssdk.services.sqs.model.Message;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
public class SqsOrderConsumer {
private final SqsClient sqsClient;
private final String queueUrl;
public SqsOrderConsumer(SqsClient sqsClient, String queueUrl) {
this.sqsClient = Objects.requireNonNull(sqsClient);
this.queueUrl = Objects.requireNonNull(queueUrl);
}
/**
* Polls messages once and processes each using the provided handler.
* @param orderHandler Consumer to process message bodies.
* @return number of messages processed.
*/
public int pollOnce(Consumer orderHandler) {
ReceiveMessageRequest receiveRequest = ReceiveMessageRequest.builder()
.queueUrl(queueUrl)
.maxNumberOfMessages(10)
.waitTimeSeconds(0)
.build();
ReceiveMessageResponse receiveResponse = sqsClient.receiveMessage(receiveRequest);
List messages = receiveResponse.messages();
for (Message message : messages) {
orderHandler.accept(message.body());
DeleteMessageRequest deleteRequest = DeleteMessageRequest.builder()
.queueUrl(queueUrl)
.receiptHandle(message.receiptHandle())
.build();
sqsClient.deleteMessage(deleteRequest);
System.out.println("Deleted receiptHandle=" + message.receiptHandle());
}
return messages.size();
}
}
The SqsOrderConsumer class demonstrates a simple Amazon SQS consumer using the AWS SDK v2. It requires a SqsClient instance and the queue URL, both checked for null in the constructor. The key method, pollOnce, polls the SQS queue for up to 10 messages without waiting, then iterates over each received message. For every message, it invokes the provided Consumer<String> handler to process the message body, followed by sending a delete request to remove the message from the queue using its receipt handle. The method logs each deletion and finally returns the count of processed messages. This approach ensures reliable message consumption with explicit deletion after successful processing, keeping the flow simple and testable.
2.3 Mocking AmazonSQS with Mockito
2.3.1 Publisher Tests
The following code demonstrates unit tests for the SqsOrderPublisher class using Mockito and JUnit 5.
package example.sqs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.SendMessageRequest;
import software.amazon.awssdk.services.sqs.model.SendMessageResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SqsOrderPublisherTest {
@Mock
SqsClient sqsClient;
private final String queueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/orders";
@InjectMocks
SqsOrderPublisher publisher = new SqsOrderPublisher(sqsClient, queueUrl);
@Test
void publish_buildsCorrectRequest_andReturnsMessageId() {
// Arrange
String body = "{\"orderId\":42}";
SendMessageResponse fakeResponse = SendMessageResponse.builder()
.messageId("mid-123")
.build();
when(sqsClient.sendMessage(any(SendMessageRequest.class))).thenReturn(fakeResponse);
// Act
String messageId = publisher.publish(body);
// Assert
assertThat(messageId).isEqualTo("mid-123");
ArgumentCaptor captor = ArgumentCaptor.forClass(SendMessageRequest.class);
verify(sqsClient, times(1)).sendMessage(captor.capture());
SendMessageRequest sentRequest = captor.getValue();
assertThat(sentRequest.queueUrl()).isEqualTo(queueUrl);
assertThat(sentRequest.messageBody()).isEqualTo(body);
assertThat(sentRequest.delaySeconds()).isEqualTo(0);
}
}
This unit test verifies the SqsOrderPublisher class by mocking the SqsClient to avoid actual AWS calls. It sets up a fake SendMessageResponse with a predefined message ID mid-123 to be returned whenever sendMessage is called. The test invokes the publish method with a sample order JSON, then asserts that the returned message ID matches the mocked response. Additionally, it captures the actual SendMessageRequest sent to the client and checks that the queue URL, message body, and delay seconds are set correctly. This test ensures that the publisher constructs the request properly and handles the client response as expected without making real network requests.
2.3.2 Consumer Test
The following code contains unit tests for the SqsOrderConsumer class, verifying message processing and deletion behavior using mocked SqsClient responses.
package example.sqs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.DeleteMessageRequest;
import software.amazon.awssdk.services.sqs.model.Message;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SqsOrderConsumerTest {
@Mock
SqsClient sqsClient;
private final String queueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/orders";
@Test
void pollOnce_processesMessages_andDeletesThem() {
// Arrange
SqsOrderConsumer consumer = new SqsOrderConsumer(sqsClient, queueUrl);
Message msg1 = Message.builder()
.body("{\"orderId\":1}")
.receiptHandle("rh-1")
.build();
Message msg2 = Message.builder()
.body("{\"orderId\":2}")
.receiptHandle("rh-2")
.build();
List messages = Arrays.asList(msg1, msg2);
ReceiveMessageResponse receiveResponse = ReceiveMessageResponse.builder()
.messages(messages)
.build();
when(sqsClient.receiveMessage(any(ReceiveMessageRequest.class))).thenReturn(receiveResponse);
AtomicInteger processedCount = new AtomicInteger(0);
Consumer handler = body -> {
System.out.println("Handled: " + body);
processedCount.incrementAndGet();
};
// Act
int count = consumer.pollOnce(handler);
// Assert
assertThat(count).isEqualTo(2);
assertThat(processedCount.get()).isEqualTo(2);
ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(DeleteMessageRequest.class);
verify(sqsClient, times(2)).deleteMessage(deleteCaptor.capture());
List deleteRequests = deleteCaptor.getAllValues();
assertThat(deleteRequests)
.extracting(DeleteMessageRequest::receiptHandle)
.containsExactlyInAnyOrder("rh-1", "rh-2");
}
@Test
void pollOnce_handlesEmptyQueue() {
// Arrange
SqsOrderConsumer consumer = new SqsOrderConsumer(sqsClient, queueUrl);
ReceiveMessageResponse emptyResponse = ReceiveMessageResponse.builder()
.messages(List.of())
.build();
when(sqsClient.receiveMessage(any(ReceiveMessageRequest.class))).thenReturn(emptyResponse);
// Act
int count = consumer.pollOnce(body -> {
throw new AssertionError("Handler should not be called on empty queue");
});
// Assert
assertThat(count).isEqualTo(0);
verify(sqsClient, never()).deleteMessage(any(DeleteMessageRequest.class));
}
}
This test class validates the behavior of the SqsOrderConsumer by mocking the SqsClient to simulate receiving messages from an SQS queue. The pollOnce_processesMessages_andDeletesThem test sets up two mock messages with distinct bodies and receipt handles, then configures the mocked client to return these messages when receiveMessage is called. It uses an atomic counter to track how many messages are processed by a handler that simply prints the message body. After polling, the test asserts that both messages were processed and verifies that deleteMessage was called for each message with the correct receipt handles. The pollOnce_handlesEmptyQueue test ensures that when no messages are returned, the handler is never invoked, and no deletion requests are made. Together, these tests confirm that the consumer correctly processes messages, deletes them from the queue, and handles empty queues without errors.
2.3.3 Code Output
The following console output shows the successful run of all unit tests for the SQS publisher and consumer, confirming correct message handling and no test failures.
Published messageId=mid-123
Handled: {"orderId":1}
Deleted receiptHandle=rh-1
Handled: {"orderId":2}
Deleted receiptHandle=rh-2
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running example.sqs.SqsOrderPublisherTest
Running example.sqs.SqsOrderConsumerTest
Results:
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
The console output confirms the successful execution of the unit tests for both the SqsOrderPublisher and SqsOrderConsumer classes. The message Published messageId=mid-123 indicates that the publisher mock returned the expected message ID. Subsequent lines show the consumer processing two messages with order IDs 1 and 2 and deleting them using their receipt handles. The test summary reports that all three tests ran without any failures, errors, or skipped tests, indicating that the mocked interactions and logic behave correctly, and the test suite completed successfully.
3. Conclusion
Mocking AmazonSQS in unit tests is essential for isolating your application’s logic and ensuring reliable, fast tests without depending on the actual AWS infrastructure. By leveraging mocking frameworks like Mockito and properly setting up your dependencies, you can simulate SQS behavior, verify interactions, and handle message processing effectively. This approach enhances test maintainability, reduces costs, and accelerates development cycles, making it a best practice for any Java developer working with AWS SQS.




