

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# 使用`mockall`适用于 Rust 的 AWS SDK 自动生成模拟
<a name="testing-automock"></a>

 适用于 Rust 的 AWS SDK 提供了多种方法来测试与 AWS 服务之交互的代码。您可以使用 `[mockall](https://docs.rs/mockall/latest/mockall)` crate 中流行的 `[automock](https://docs.rs/mockall/latest/mockall/attr.automock.html)` 来自动生成测试所需的大多数 mock 实现。

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

1. 在项目目录的命令提示符中，将 `[mockall](https://docs.rs/mockall/latest/mockall)` crate 添加为依赖项：

   ```
   $ cargo add --dev mockall
   ```

   使用 `--dev` [选项](https://doc.rust-lang.org/cargo/commands/cargo-add.html)可将 crate 添加到 `Cargo.toml` 文件的 `[dev-dependencies]` 部分。作为[开发依赖项](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#development-dependencies)，它不会被编译并包含在用于生产代码的最终二进制文件中。

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

   ```
   $ cargo add aws-sdk-s3
   ```

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

1. 添加 `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};
   ```

1. 接下来，添加代码，确定要使用应用程序的 Amazon S3 包装器结构的两个实现中的哪一个。
   + 用于通过网络访问 Amazon S3 的真正实现。
   + `mockall` 生成的 mock 实现。

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

   ```
   #[cfg(test)]
   pub use MockS3Impl as S3;
   #[cfg(not(test))]
   pub use S3Impl as S3;
   ```

1. 该`S3Impl`结构是实际向发送请求的 Amazon S3 包装器结构的实现。 AWS
   + 启用测试后，将不使用此代码，因为请求已发送到 mock 而不是 AWS。`dead_code` 属性告诉 linter，如果未使用 `S3Impl` 类型，不要报告问题。
   +  条件 `#[cfg_attr(test, automock)]` 表示启用测试后，应设置 `automock` 属性。这会告诉 `mockall` 生成一个名为 `MockS3Impl` 的 `S3Impl` 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
       }
   }
   ```

1. 在名为 `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` 实例。
   + 它使用 mock 的 `expect_list_objects()` 方法（由 `automock` 自动创建）来设置当在代码的其他地方使用 `list_objects()` 方法时的预期结果。
   + 在设置预期结果后，系统会通过调用 `determine_prefix_file_size()` 使用这些结果来测试函数。使用断言检查返回值以确认其是否正确。

1. `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 请求时同时支持 `S3Impl` 和 `MockS3Impl`。启用测试后，由 `mockall` 自动生成的 mock 会报告任何测试失败的情况。

您可以在[上查看这些示例的完整代码](https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/rustv1/examples/testing) GitHub。