AWS SDK for Rust で aws-smithy-mocks を使用したユニットテスト
AWS SDK for Rust では、AWS のサービスとやり取りするコードをテストするために複数のアプローチを利用できます。このトピックでは、テスト目的で AWS SDK クライアントのレスポンスのモックを作成するため、シンプルで強力な方法を提供する aws-smithy-mocks
概要
AWS のサービスを使用するコードのテストを記述する場合は、実際のネットワーク呼び出しを避けることがよくあります。aws-smithy-mocks クレートを使用すると、以下を実現するソリューションを提供します。
-
SDK が特定のリクエストにどのように応答するかを定義するモックルールを作成する。
-
さまざまなタイプのレスポンス (成功、エラー、HTTP レスポンス) を返す。
-
プロパティに基づいてリクエストを一致させる。
-
再試行動作をテストするための応答のシーケンスを定義する。
-
ルールが想定どおりに使用されたことを確認する。
依存関係の追加
プロジェクトディレクトリのコマンドプロンプトで、aws-smithy-mocks
$cargo add --dev aws-smithy-mocks
--dev オプションCargo.toml ファイルの [dev-dependencies] セクションにクレートが追加されます。開発の依存関係
また、このサンプルコードは、Amazon Simple Storage Service を AWS のサービス の例として使用しており、test-util 機能が必要です。
$cargo add aws-sdk-s3 --features test-util
これにより、[dependencies] ファイルの Cargo.toml セクションにクレートが追加されます。
基本的な使用法
aws-smithy-mocks を使用して Amazon Simple Storage Service (Amazon S3) とやり取りするコードをテストする方法の簡単な例を以下に示します。
use aws_sdk_s3::operation::get_object::GetObjectOutput; use aws_sdk_s3::primitives::ByteStream; use aws_smithy_mocks::{mock, mock_client}; #[tokio::test] async fn test_s3_get_object() { // Create a rule that returns a successful response let get_object_rule = mock!(aws_sdk_s3::Client::get_object) .then_output(|| { GetObjectOutput::builder() .body(ByteStream::from_static(b"test-content")) .build() }); // Create a mocked client with the rule let s3 = mock_client!(aws_sdk_s3, [&get_object_rule]); // Use the client as you would normally let result = s3 .get_object() .bucket("test-bucket") .key("test-key") .send() .await .expect("success response"); // Verify the response let data = result.body.collect().await.expect("successful read").to_vec(); assert_eq!(data, b"test-content"); // Verify the rule was used assert_eq!(get_object_rule.num_calls(), 1); }
モックルールの作成
ルールは、mock! マクロを使用して作成されます。マクロは、クライアントオペレーションを引数として受け取ります。その後、ルールがどのように動作するかを設定できます。
リクエストの一致
リクエストのプロパティを一致させることにより、ルールをより具体化できます。
let rule = mock!(Client::get_object) .match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("test-key")) .then_output(|| { GetObjectOutput::builder() .body(ByteStream::from_static(b"test-content")) .build() });
さまざまなレスポンスタイプ
さまざまなタイプのレスポンスを返すことができます。
// Return a successful response let success_rule = mock!(Client::get_object) .then_output(|| GetObjectOutput::builder().build()); // Return an error let error_rule = mock!(Client::get_object) .then_error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build())); // Return a specific HTTP response let http_rule = mock!(Client::get_object) .then_http_response(|| { HttpResponse::new( StatusCode::try_from(503).unwrap(), SdkBody::from("service unavailable") ) });
再試行動作のテスト
aws-smithy-mocks の最も強力な機能の 1 つは、レスポンスのシーケンスを定義して再試行動作をテストできることです。
// Create a rule that returns 503 twice, then succeeds let retry_rule = mock!(aws_sdk_s3::Client::get_object) .sequence() .http_status(503, None) // First call returns 503 .http_status(503, None) // Second call returns 503 .output(|| GetObjectOutput::builder().build()) // Third call succeeds .build(); // With repetition using times() let retry_rule = mock!(Client::get_object) .sequence() .http_status(503, None) .times(2) // First two calls return 503 .output(|| GetObjectOutput::builder().build()) // Third call succeeds .build();
ルールモード
ルールモードを使用して、ルールの一致と適用方法を以下のように制御できます。
// Sequential mode: Rules are tried in order, and when a rule is exhausted, the next rule is used let client = mock_client!(aws_sdk_s3, RuleMode::Sequential, [&rule1, &rule2]); // MatchAny mode: The first matching rule is used, regardless of order let client = mock_client!(aws_sdk_s3, RuleMode::MatchAny, [&rule1, &rule2]);
例: 再試行動作のテスト
再試行動作をテストする方法を示す、より完全な例を以下に示します。
use aws_sdk_s3::operation::get_object::GetObjectOutput; use aws_sdk_s3::config::RetryConfig; use aws_sdk_s3::primitives::ByteStream; use aws_smithy_mocks::{mock, mock_client, RuleMode}; #[tokio::test] async fn test_retry_behavior() { // Create a rule that returns 503 twice, then succeeds let retry_rule = mock!(aws_sdk_s3::Client::get_object) .sequence() .http_status(503, None) .times(2) .output(|| GetObjectOutput::builder() .body(ByteStream::from_static(b"success")) .build()) .build(); // Create a mocked client with the rule and custom retry configuration let s3 = mock_client!( aws_sdk_s3, RuleMode::Sequential, [&retry_rule], |client_builder| { client_builder.retry_config(RetryConfig::standard().with_max_attempts(3)) } ); // This should succeed after two retries let result = s3 .get_object() .bucket("test-bucket") .key("test-key") .send() .await .expect("success after retries"); // Verify the response let data = result.body.collect().await.expect("successful read").to_vec(); assert_eq!(data, b"success"); // Verify all responses were used assert_eq!(retry_rule.num_calls(), 3); }
例: リクエストパラメータに基づくさまざまなレスポンス
リクエストパラメータに基づいて、異なるレスポンスを返すルールを作成することもできます。
use aws_sdk_s3::operation::get_object::{GetObjectOutput, GetObjectError}; use aws_sdk_s3::types::error::NoSuchKey; use aws_sdk_s3::Client; use aws_sdk_s3::primitives::ByteStream; use aws_smithy_mocks::{mock, mock_client, RuleMode}; #[tokio::test] async fn test_different_responses() { // Create rules for different request parameters let exists_rule = mock!(Client::get_object) .match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("exists")) .sequence() .output(|| GetObjectOutput::builder() .body(ByteStream::from_static(b"found")) .build()) .build(); let not_exists_rule = mock!(Client::get_object) .match_requests(|req| req.bucket() == Some("test-bucket") && req.key() == Some("not-exists")) .sequence() .error(|| GetObjectError::NoSuchKey(NoSuchKey::builder().build())) .build(); // Create a mocked client with the rules in MatchAny mode let s3 = mock_client!(aws_sdk_s3, RuleMode::MatchAny, [&exists_rule, ¬_exists_rule]); // Test the "exists" case let result1 = s3 .get_object() .bucket("test-bucket") .key("exists") .send() .await .expect("object exists"); let data = result1.body.collect().await.expect("successful read").to_vec(); assert_eq!(data, b"found"); // Test the "not-exists" case let result2 = s3 .get_object() .bucket("test-bucket") .key("not-exists") .send() .await; assert!(result2.is_err()); assert!(matches!(result2.unwrap_err().into_service_error(), GetObjectError::NoSuchKey(_))); }
ベストプラクティス
テストに aws-smithy-mocks を使用する場合:
-
特定のリクエストに一致:
match_requests()を使用して、ルールが意図したリクエスト (特にRuleMode:::MatchAny) にのみ適用されるようにします。 -
ルールの使用状況を確認:
rule.num_calls()を確認し、ルールが実際に使用されたことを確認します。 -
テストエラー処理: エラーを返すルールを作成して、コードによって失敗を処理する方法をテストします。
-
テスト再試行ロジック: レスポンスシーケンスを使用して、コードによってカスタム再試行分類子やその他の再試行動作を正しく処理することを確認します。
-
テストに集中: 1 つのテストですべてを網羅しようとするのではなく、シナリオごとに個別のテストを作成します。