Enterprise Java

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.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button