AWS SDK for Rust で mockall を使用してモックを自動的に生成する
AWS SDK for Rust では、AWS のサービスとやり取りするコードをテストするために複数のアプローチを利用できます。mockall クレートの一般的な automock を使用することにより、テストに必要なモック実装の大部分を自動的に生成できます。
この例では、determine_prefix_file_size() というカスタムメソッドをテストします。このメソッドは、Amazon S3 を呼び出すカスタム list_objects() ラッパーメソッドを呼び出します。list_objects() のモックを作成することにより、Amazon S3 と実際にやり取りすることなく determine_prefix_file_size() メソッドをテストできます。
-
プロジェクトディレクトリのコマンドプロンプトで、
mockallクレートを次の依存関係として追加します。$cargo add --dev mockall--devオプションを使用すると、 Cargo.tomlファイルの[dev-dependencies]セクションにクレートが追加されます。開発の依存関係として扱われるため、これはコンパイルされず、本番コードで使用される最終バイナリには含まれません。 また、このサンプルコードは、AWS のサービス の例として Amazon Simple Storage Service を使用します。
$cargo add aws-sdk-s3これにより、
[dependencies]ファイルのCargo.tomlセクションにクレートが追加されます。 -
mockallクレートからautomockモジュールを含めます。また、テストする AWS のサービス に関連する他のライブラリ (この場合は Amazon S3) をすべて含めてください。
use aws_sdk_s3 as s3; #[allow(unused_imports)] use mockall::automock; use s3::operation::list_objects_v2::{ListObjectsV2Error, ListObjectsV2Output}; -
次に、アプリケーションの Amazon S3 ラッパー構造の 2 つの実装のうち、どちらを使用するかを決定するコードを追加します。
-
ネットワーク経由で Amazon S3 にアクセスするために実際に記述された実装。
-
mockallによって生成されたモック実装。
この例では、選択した実装に
S3という名前が付けられています。選択したものは、以下のようにtest属性に基づく条件が付いています。#[cfg(test)] pub use MockS3Impl as S3; #[cfg(not(test))] pub use S3Impl as S3; -
-
S3Impl構造は、AWS にリクエストを実際に送信する Amazon S3 ラッパー構造の実装です。-
テストが有効になっている場合、リクエストは AWS ではなくモックに送信されるため、このコードは使用されません。
dead_code属性は、S3Implタイプが使用されていない場合は問題を報告しないように linter に指示します。 -
この条件付き
#[cfg_attr(test, automock)]は、テストが有効になっている場合、automock属性を設定する必要があることを示します。これにより、Mockという名前のS3ImplS3Implのモックを生成するようにmockallに指示します。 -
この例では、
list_objects()メソッドはモックを作成する呼び出しです。automockは自動的にexpect_メソッドを作成します。list_objects()
#[allow(dead_code)] pub struct S3Impl { inner: s3::Client, } #[cfg_attr(test, automock)] impl S3Impl { #[allow(dead_code)] pub fn new(inner: s3::Client) -> Self { Self { inner } } #[allow(dead_code)] pub async fn list_objects( &self, bucket: &str, prefix: &str, continuation_token: Option<String>, ) -> Result<ListObjectsV2Output, s3::error::SdkError<ListObjectsV2Error>> { self.inner .list_objects_v2() .bucket(bucket) .prefix(prefix) .set_continuation_token(continuation_token) .send() .await } } -
-
testという名前のモジュールでテスト関数を作成します。-
この条件付き
#[cfg(test)]は、test属性がtrueの場合、mockallによってテストモジュールを構築する必要があることを示します。
#[cfg(test)] mod test { use super::*; use mockall::predicate::eq; #[tokio::test] async fn test_single_page() { let mut mock = MockS3Impl::default(); mock.expect_list_objects() .with(eq("test-bucket"), eq("test-prefix"), eq(None)) .return_once(|_, _, _| { Ok(ListObjectsV2Output::builder() .set_contents(Some(vec![ // Mock content for ListObjectsV2 response s3::types::Object::builder().size(5).build(), s3::types::Object::builder().size(2).build(), ])) .build()) }); // Run the code we want to test with it let size = determine_prefix_file_size(mock, "test-bucket", "test-prefix") .await .unwrap(); // Verify we got the correct total size back assert_eq!(7, size); } #[tokio::test] async fn test_multiple_pages() { // Create the Mock instance with two pages of objects now let mut mock = MockS3Impl::default(); mock.expect_list_objects() .with(eq("test-bucket"), eq("test-prefix"), eq(None)) .return_once(|_, _, _| { Ok(ListObjectsV2Output::builder() .set_contents(Some(vec![ // Mock content for ListObjectsV2 response s3::types::Object::builder().size(5).build(), s3::types::Object::builder().size(2).build(), ])) .set_next_continuation_token(Some("next".to_string())) .build()) }); mock.expect_list_objects() .with( eq("test-bucket"), eq("test-prefix"), eq(Some("next".to_string())), ) .return_once(|_, _, _| { Ok(ListObjectsV2Output::builder() .set_contents(Some(vec![ // Mock content for ListObjectsV2 response s3::types::Object::builder().size(3).build(), s3::types::Object::builder().size(9).build(), ])) .build()) }); // Run the code we want to test with it let size = determine_prefix_file_size(mock, "test-bucket", "test-prefix") .await .unwrap(); assert_eq!(19, size); } }-
各テストでは、
let mut mock = MockS3Impl::default();を使用してMockS3Implのmockインスタンスを作成します。 -
モックの
expect_list_objects()メソッド (automockによって自動的に作成された) を使用して、list_objects()メソッドがコードの他の場所で使用された場合に想定される結果を設定します。 -
想定値が設定されたら、これらを使用して
determine_prefix_file_size()を呼び出して関数をテストします。返された値は、アサーションを使用して、正しいことを確認するために検証します。
-
-
determine_prefix_file_size()関数は、Amazon S3 ラッパーを使用してプレフィックスファイルのサイズを取得します。#[allow(dead_code)] pub async fn determine_prefix_file_size( // Now we take a reference to our trait object instead of the S3 client // s3_list: ListObjectsService, s3_list: S3, bucket: &str, prefix: &str, ) -> Result<usize, s3::Error> { let mut next_token: Option<String> = None; let mut total_size_bytes = 0; loop { let result = s3_list .list_objects(bucket, prefix, next_token.take()) .await?; // Add up the file sizes we got back for object in result.contents() { total_size_bytes += object.size().unwrap_or(0) as usize; } // Handle pagination, and break the loop if there are no more pages next_token = result.next_continuation_token.clone(); if next_token.is_none() { break; } } Ok(total_size_bytes) }
S3 型は、HTTP リクエストを行うときに、ラップされた SDK for Rust 関数を呼び出して S3Impl と MockS3Impl の両方をサポートするために使用されます。テストが有効になっている場合、mockall によって自動的に生成されたモックはテスト失敗を報告します。
GitHub でこれらの例の完全なコードを表示