使用适用于 Rust 的 AWS SDK 中的 mockall 自动生成 mock - 适用于 Rust 的 AWS SDK

使用适用于 Rust 的 AWS SDK 中的 mockall 自动生成 mock

适用于 Rust 的 AWS SDK 提供了多种方法来测试与 AWS 服务交互的代码。您可以使用 mockall crate 中流行的 automock 来自动生成测试所需的大多数 mock 实现。

此示例测试一个名为 determine_prefix_file_size() 的自定义方法。此方法调用一个调用 Amazon S3 的自定义 list_objects() 包装器方法。通过模拟 list_objects(),无需实际联系 Amazon S3 即可测试 determine_prefix_file_size() 方法。

  1. 在项目目录的命令提示符中,将 mockall crate 添加为依赖项:

    $ cargo add --dev mockall

    使用 --dev 选项可将 crate 添加到 Cargo.toml 文件的 [dev-dependencies] 部分。作为开发依赖项,它不会被编译并包含在用于生产代码的最终二进制文件中。

    此示例代码还使用 Amazon Simple Storage Service 作为示例 AWS 服务。

    $ cargo add aws-sdk-s3

    这会将 crate 添加到 Cargo.toml 文件的 [dependencies] 部分。

  2. 添加 mockall crate 中的 automock 模块。

    同时,还要添加与您正在测试的 AWS 服务(在本例中为 Amazon S3)相关的任何其他库。

    use aws_sdk_s3 as s3; #[allow(unused_imports)] use mockall::automock; use s3::operation::list_objects_v2::{ListObjectsV2Error, ListObjectsV2Output};
  3. 接下来,添加代码,确定要使用应用程序的 Amazon S3 包装器结构的两个实现中的哪一个。

    • 用于通过网络访问 Amazon S3 的真正实现。

    • mockall 生成的 mock 实现。

    在这个示例中,选中的一个名称为 S3。选择是基于 test 属性的条件进行的:

    #[cfg(test)] pub use MockS3Impl as S3; #[cfg(not(test))] pub use S3Impl as S3;
  4. S3Impl 结构是实际向 AWS 发送请求的 Amazon S3 包装器结构的实现。

    • 启用测试后,将不使用此代码,因为请求已发送到 mock 而不是 AWS。dead_code 属性告诉 linter,如果未使用 S3Impl 类型,不要报告问题。

    • 条件 #[cfg_attr(test, automock)] 表示启用测试后,应设置 automock 属性。这会告诉 mockall 生成一个名为 MockS3ImplS3Impl mock。

    • 在此示例中,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 } }
  5. 在名为 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(); 来创建 MockS3Implmock 实例。

    • 它使用 mock 的 expect_list_objects() 方法(由 automock 自动创建)来设置当在代码的其他地方使用 list_objects() 方法时的预期结果。

    • 在设置预期结果后,系统会通过调用 determine_prefix_file_size() 使用这些结果来测试函数。使用断言检查返回值以确认其是否正确。

  6. 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 用于调用封装的适用于 Rust 的 SDK 函数,以便在发出 HTTP 请求时同时支持 S3ImplMockS3Impl。启用测试后,由 mockall 自动生成的 mock 会报告任何测试失败的情况。

您可以在 GitHub 上查看完整的示例代码