如何在單元測試中模擬 AmazonSQS
1.概述
單元測試不應依賴網路連線。測試與 Amazon SQS 互動的程式碼時,最簡潔的方法是使用適用於Java 的 AWS 開發工具包用戶端(例如SqsClient或SqsAsyncClient的相依性注入。
在我們的測試中,我們可以用模擬物件替換真實的客戶端。這樣,我們就可以驗證程式碼是否建置了我們期望的SendMessageRequest ,而無需實際向 AWS 發送訊息。這種方法可以使您的測試快速、確定且無需憑證。
在本教程中,我們將討論如何在單元測試中模擬 Amazon SQS。
2. 依賴關係
讓我們先將Amazon SQS 依賴項加入到我們專案的pom.xml檔案中:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
<version>2.35.10</version>
</dependency>
此相依性為我們提供了SqsClient 、 SqsAsyncClient和相關的請求/回應模型,我們將使用它們與 Amazon SQS 服務進行互動。
3. 被測服務
在編寫測試之前,我們需要一個元件來測試。讓我們建立一個簡單的服務SqsMessagePublisher ,它的唯一職責是向指定的 SQS 佇列發送訊息。
此服務的關鍵設計元素是依賴注入的使用。我們沒有在類別內部創建SqsClient實例,而是將其作為建構函數參數接受。這種設計選擇至關重要,因為它將我們的業務邏輯與 SQS 用戶端的具體實作分開。這種分離使得該類別易於測試,使我們能夠在測試中提供模擬客戶端,而不是真實的客戶端。
public class SqsMessagePublisher {
private final SqsClient sqsClient;
public SqsMessagePublisher(SqsClient sqsClient) {
this.sqsClient = sqsClient;
}
public String publishMessage(String queueUrl, String messageBody) {
SendMessageRequest request = SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody(messageBody)
.build();
SendMessageResponse response = sqsClient.sendMessage(request);
return response.messageId();
}
}
publishMessage()方法接受佇列 URL 和訊息正文,建構一個SendMessageRequest對象,使用注入的SqsClient發送該請求,並從回應中傳回訊息 ID。單元測試的目標是驗證傳遞給sqsClient.sendMessage() SendMessageRequest物件是否使用正確的佇列 URL 和訊息正文建構。
4. 模擬同步SqsClient
服務部署完畢後,我們現在可以寫單元測試了。我們將使用 JUnit 5 作為測試運行器,並使用 Mockito 建立SqsClient的模擬版本。
4.1. 測試設置
首先,讓我們設定測試類別。我們將使用三個關鍵註解:
-
@ExtendWith(MockitoExtension.class):此註釋將 Mockito 與 JUnit 5 測試生命週期集成,從而可以使用其他 Mockito 註釋 -
@Mock:這將創建帶有註解字段的模擬實現。在本例中,它將建立一個模擬的SqsClient -
@InjectMocks:這會建立帶有註解類別的實例,並將所有帶有@Mock註解的欄位注入其中。這會自動將我們的模擬 SQS 客戶端連接到我們的SqsMessagePublisher實例。
@ExtendWith(MockitoExtension.class)
class SqsMessagePublisherUnitTest {
@Mock
private SqsClient sqsClient;
@InjectMocks
private SqsMessagePublisher messagePublisher;
// Tests will go here
}
4.2. 編寫測試
我們測試的核心是驗證我們的服務是否使用正確的參數來呼叫 SQS 用戶端。為此,我們首先存根客戶端的回應以防止NullPointerException 。然後,我們使用ArgumentCaptor捕獲發送到模擬客戶端的請求並斷言其內容:
@Test
void whenPublishMessage_thenMessageIsSentWithCorrectParameters() {
// Arrange
String queueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue";
String messageBody = "Hello, SQS!";
String expectedMessageId = "test-message-id-123";
SendMessageResponse mockResponse = SendMessageResponse.builder()
.messageId(expectedMessageId)
.build();
when(sqsClient.sendMessage(any(SendMessageRequest.class))).thenReturn(mockResponse);
// Act
String actualMessageId = messagePublisher.publishMessage(queueUrl, messageBody);
// Assert
assertThat(actualMessageId).isEqualTo(expectedMessageId);
ArgumentCaptor<SendMessageRequest> requestCaptor = ArgumentCaptor.forClass(SendMessageRequest.class);
verify(sqsClient).sendMessage(requestCaptor.capture());
SendMessageRequest capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest.queueUrl()).isEqualTo(queueUrl);
assertThat(capturedRequest.messageBody()).isEqualTo(messageBody);
}
在「Arrange」階段,我們使用when().thenReturn()來告訴我們的模擬sqsClient回傳什麼。在「Assert」階段, verify(sqsClient).sendMessage()確認方法已被調用,並且ArgumentCaptor允許我們檢查傳遞給它的SendMessageRequest 。
5. 模擬同步SqsClient
相同的測試策略適用於非阻塞SqsAsyncClient:
public class SqsAsyncMessagePublisher {
private final SqsAsyncClient sqsAsyncClient;
public SqsAsyncMessagePublisher(SqsAsyncClient sqsAsyncClient) {
this.sqsAsyncClient = sqsAsyncClient;
}
public CompletableFuture<String> publishMessage(String queueUrl, String messageBody) {
SendMessageRequest request = SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody(messageBody)
.build();
return sqsAsyncClient.sendMessage(request)
.thenApply(SendMessageResponse::messageId);
}
}
測試時,唯一的變化在於我們如何存根回應。由於非同步客戶端回傳一個CompletableFuture ,我們必須使用CompletableFuture.completedFuture():
@ExtendWith(MockitoExtension.class)
class SqsAsyncMessagePublisherUnitTest {
@Mock
private SqsAsyncClient sqsAsyncClient;
@InjectMocks
private SqsAsyncMessagePublisher messagePublisher;
@Test
void whenPublishMessage_thenMessageIsSentAsynchronously() throws Exception {
// Arrange
String queueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/MyAsyncQueue";
String messageBody = "Hello, Async SQS!";
String expectedMessageId = "test-async-message-id-456";
SendMessageResponse mockResponse = SendMessageResponse.builder()
.messageId(expectedMessageId)
.build();
when(sqsAsyncClient.sendMessage(any(SendMessageRequest.class)))
.thenReturn(CompletableFuture.completedFuture(mockResponse));
// Act
String actualMessageId = messagePublisher.publishMessage(queueUrl, messageBody).get();
// Assert
assertThat(actualMessageId).isEqualTo(expectedMessageId);
ArgumentCaptor<SendMessageRequest> requestCaptor = ArgumentCaptor.forClass(SendMessageRequest.class);
verify(sqsAsyncClient).sendMessage(requestCaptor.capture());
SendMessageRequest capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest.queueUrl()).isEqualTo(queueUrl);
assertThat(capturedRequest.messageBody()).isEqualTo(messageBody);
}
}
ArgumentCaptor的驗證邏輯保持不變,證明了此測試模式的穩健性。
6. 關於整合測試的說明
這些單元測試非常適合單獨驗證應用程式的邏輯。然而,它們無法驗證實際問題,例如 IAM 權限、網路連接或正確的佇列配置。
為此,我們需要進行整合測試。常見的方法是使用 Testcontainers 等工具和 LocalStack,後者提供了 AWS 服務的本地模擬。這樣,您就可以測試與真實 SQS 端點的完整集成,而無需實際的 AWS 憑證。
7. 結論
在本文中,我們示範如何對與 Amazon SQS 互動的程式碼進行單元測試。透過使用依賴注入,我們可以輕鬆地在測試中用模擬程式碼取代真實的 SQS 用戶端。
我們利用 Mockito 對客戶端行為進行了存根處理,並使用ArgumentCaptor來驗證程式碼是否建置了正確的請求。這項強大的技術適用於同步和非同步客戶端,使我們能夠編寫快速、可靠且獨立的單元測試。
與往常一樣,程式碼可在 GitHub 上取得。