Upgrade to Pro — share decks privately, control downloads, hide ads and more …

aws-smithy-mocksを使ってみよう

Avatar for Kikuo Emoto Kikuo Emoto
September 30, 2025

 aws-smithy-mocksを使ってみよう

aws-smithy-mocksと実践的なテストの書き方の紹介

Avatar for Kikuo Emoto

Kikuo Emoto

September 30, 2025
Tweet

More Decks by Kikuo Emoto

Other Decks in Programming

Transcript

  1. 自己紹介 Kikuo (江本 喜久男) フリーランスの何でも屋 (屋号: codemonger) 推し技術: Rust, AWS Serverless,

    AWS CDK プログラミング以外の趣味: 絵、走ること 好きなアイスホッケーチーム: Pittsburgh Penguins kikuomax (https://github.com/kikuomax) codemonger (https://github.com/codemonger-io)
  2. 簡単なテスト ステップバイステップの解説はこの後 → #[tokio::test] async fn test_dynamodb_table_put_item() { // 1.

    モックの挙動を定義 let put_item_ok = mock!(aws_sdk_dynamodb::Client::put_item) .then_output(|| PutItemOutput::builder().build()); // 2. モックの挙動を指定してクライアントを初期化 let dynamodb = mock_client!(aws_sdk_dynamodb, [&put_item_ok]); // 3. モックしたクライアントでテストしたい関数を実行 let result = my_function(dynamodb).await; // 4. 結果を検証 assert!(result.is_ok()); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
  3. SharedState Lambdaインスタンスのライフサイクルを通して再利用する パラメータを保持 (余談) テスト時に に対応すると便利 ( クレートを使うとお手軽) #[derive(derive_builder::Builder)] struct

    SharedState { /// Cognito クライアント cognito: aws_sdk_cognitoidentityprovider::Client, /// DynamoDB クライアント dynamodb: aws_sdk_dynamodb::Client, } 1 2 3 4 5 6 7 ビルダーパターン derive_builder
  4. SharedState メイン関数で初期化し、Lambda呼び出しのたびにハンドラ に渡す 非同期(async )呼び出しを越えて共有するので でラップ use lambda_runtime::{Error, run, service_fn};

    use std::sync::Arc; #[tokio::main] async fn main() -> Result<(), Error> { let shared_state = Arc::new(SharedState::new().await?); run(service_fn(|req| async { function_handler(shared_state.clone(), req).await })).await } 1 2 3 4 5 6 7 8 9 10 Arc
  5. とっちらかしてみる モックの挙動定義を分離せずに書くとこんな感じ // 長々としたuse 宣言 use aws_sdk_cognitoidentityprovider::{ Client as CognitoClient,

    operation::{ admin_create_user::AdminCreateUserOutput, admin_set_user_password::AdminSetUserPasswordOutput, list_users::ListUsersOutput, }, types::{AttributeType, UserType}, }; use aws_sdk_dynamodb::{ Client as DynamodbClient, operation::{ delete_item::DeleteItemOutput, put_item::PutItemOutput, }, primitives::DateTime, types::AttributeValue, }; use aws_smithy_mocks::{mock, mock_client, RuleMode}; use std::collections::HashMap; use std::time::SystemTime; #[tokio::test] async fn finish_registration_of_legitimate_new_user() { // Cognito の挙動をモックする let list_users_empty = mock!(CognitoClient::list_users) .then_output(|| ListUsersOutput::builder().build()); let admin_create_user_ok = mock!(CognitoClient::admin_create_user) .then_output(|| AdminCreateUserOutput::builder() .user(UserType::builder() .attributes(AttributeType::builder() .name("sub") .value("dummy-sub-123") .build() .unwrap()) .build()) .build()); let admin_set_user_password_ok = mock!(CognitoClient::admin_set_user_password) .then_output(|| AdminSetUserPasswordOutput::builder().build()); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // DynamoDB の挙動をモックする let delete_item_session = mock!(DynamodbClient::delete_item) .then_output(|| { let ttl = DateTime::from(SystemTime::now()).secs() + 300; DeleteItemOutput::builder() .attributes("ttl", AttributeValue::N(format!("{}", ttl))) .attributes("state", AttributeValue::S(OK_PASSKEY_REGISTRATION.to_string())) .attributes("userId", AttributeValue::S("8TZ_kg_dp_pr0t7SDvGJiw".to_string())) .attributes("userInfo", AttributeValue::M(HashMap::from([ ("username".to_string(), AttributeValue::S("test".to_string())), ("displayName".to_string(), AttributeValue::S("Test User".to_string())), ]))) .build() }); let put_item_ok = mock!(DynamodbClient::put_item) .then_output(|| PutItemOutput::builder().build()); // SharedState を初期化する let shared_state = SharedStateBuilder::default() .webauthn(ConstantWebauthn::new(OK_PASSKEY)) .cognito(cognito) .dynamodb(dynamodb) .build() .unwrap(); let shared_state = Arc::new(shared_state); // 関数を検証する let res = finish_registration( shared_state, FinishRegistrationSession { session_id: "dummy-session-id".to_string(), public_key_credential: serde_json::from_str( OK_REGISTER_PUBLIC_KEY_CREDENTIAL, ).unwrap(), }, ).await.unwrap(); assert_eq!(res.user_id, "8TZ_kg_dp_pr0t7SDvGJiw"); } 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
  6. フィクスチャの例 - その2 DynamoDBの APIをモックする例 pub(crate) fn delete_item_session() -> Rule

    { mock!(Client::delete_item) .then_output(|| { let ttl = DateTime::from(SystemTime::now()).secs() + 300; DeleteItemOutput::builder() .attributes("ttl", AttributeValue::N(format!("{}", ttl))) .attributes("state", AttributeValue::S(OK_PASSKEY_REGISTRATION.to_string())) .attributes("userId", AttributeValue::S("8TZ_kg_dp_pr0t7SDvGJiw".to_string())) .attributes("userInfo", AttributeValue::M(HashMap::from([ ("username".to_string(), AttributeValue::S("test".to_string())), ("displayName".to_string(), AttributeValue::S("Test User".to_string())), ]))) .build() }) } DeleteItem
  7. AWSサービスごとにサブモジュール化 Cognitoのモックの挙動定義 → mocks::cognito DynamoDBのモックの挙動定義 → mocks::dynamodb pub(crate) mod mocks

    { use aws_smithy_mocks::{mock, Rule}; pub(crate) mod cognito { // ... use 宣言省略 pub(crate) fn list_users_empty() -> Rule { /* 省略 */ } pub(crate) fn admin_create_user_ok() -> Rule { /* 省略 */ } pub(crate) fn admin_set_user_password_ok() -> Rule { /* 省略 */ } } pub(crate) mod dynamodb { // ... use 宣言省略 pub(crate) fn delete_item_session() -> Rule { /* 省略 */ } pub(crate) fn put_item_ok() -> Rule { /* 省略 */ } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14
  8. すっきりまとめる ← モッククライアント構築部分がスッキリ use aws_smithy_mocks::{mock_client, RuleMode}; #[tokio::test] async fn finish_registration_of_legitimate_new_user()

    { // モックの挙動を選択してCognito クライアントを構築する let cognito = mock_client!( aws_sdk_cognitoidentityprovider, RuleMode::MatchAny, [ &self::mocks::cognito::list_users_empty(), &self::mocks::cognito::admin_create_user_ok(), &self::mocks::cognito::admin_set_user_password_ok(), ] ); // モックの挙動を選択してDynamoDB クライアントを構築する let dynamodb = mock_client!( aws_sdk_dynamodb, RuleMode::MatchAny, [ &self::mocks::dynamodb::delete_item_session(), &self::mocks::dynamodb::put_item_ok(), ] ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // SharedState を初期化する let shared_state = SharedStateBuilder::default() .webauthn(ConstantWebauthn::new(OK_PASSKEY)) .cognito(cognito) .dynamodb(dynamodb) .build() .unwrap(); let shared_state = Arc::new(shared_state); // 関数を検証する let res = finish_registration( shared_state, FinishRegistrationSession { session_id: "dummy-session-id".to_string(), public_key_credential: serde_json::from_str( OK_REGISTER_PUBLIC_KEY_CREDENTIAL, ).unwrap(), }, ).await.unwrap(); assert_eq!(res.user_id, "8TZ_kg_dp_pr0t7SDvGJiw"); } 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
  9. モデルエラーをモックする モデルエラー(modeled error) = enum のバリアントとして定 義されているエラー (例: ) メソッドで設定

    code は明示的に設定すべし pub(crate) fn put_item_throttling_exception() -> Rule { mock!(Client::put_item) .then_error(|| PutItemError::ThrottlingException( ThrottlingException::builder() .meta(ErrorMetadata::builder() .code("ThrottlingException") .build()) .build(), )) } PutItemError::ThrottlingException RuleBuilder::then_error
  10. 非モデルエラーをモックする すべてのエラーがenum のバリアントとして定義されているわ けではない (例: ServiceUnavailable ) でJSONを設定 code がエラータイプを表す

    HTTPステータスコードの設定も必要 pub(crate) fn list_users_service_unavailable() -> Rule { mock!(Client::list_users) .then_http_response(|| { HttpResponse::new( StatusCode::try_from(503).unwrap(), SdkBody::from(r#"{ "code": "ServiceUnavailable", "message": "Service unavailable." }"#), ) }) } RuleBuilder::then_http_reponses