AWS

テスト時のboto3.clientモック化の落とし穴とその解決策

Champ

どうも、Champです🙌
先日、AWSのCognitoサービスのadmin_get_user APIをpytestとunittest.mockを用いてテストしようとした際に、ちょっとしたハマりポイントがあったので、その経験を皆さんと共有したいと思います。AWSを使った開発を行っている方々、特に単体テストの際に外部サービスとの接続をモック化する必要がある方にとって、少しでも参考になればと思います。
それでは、具体的な問題とその解決方法について見ていきましょう。

問題の概要

AWSのCognitoサービスのadmin_get_user APIをpytestとunittest.mockを用いてテストしようとした際に、botocore.exceptions.NoCredentialsError: Unable to locate credentialsとエラーが発生。

原因の解析

以下の様に関数・テストを記述していたものの、テスト実行時にboto3クライアントをモック化する前に実際のboto3.client('cognito-idp')が実行されてしまっていました。
したがって、AWS認証情報を探しに行くが、認証情報を見つけることができずNoCredentialsErrorが発生していました。

テスト対象関数

cognito = boto3.client('cognito-idp')

def get_user_attributes(username):
    cognito_user_pool_id = os.environ['USER_POOL_ID']
    return cognito.admin_get_user(
        UserPoolId=cognito_user_pool_id,
        Username=username
    )

pytestのコード

@patch("boto3.client")
def test_get_user_attributes(mock_client):
    mock_cognito = MagicMock()
    mock_client.return_value = mock_cognito
    os.environ["USER_POOL_ID"] = "dummy_pool_id"
    username = "検証に使用するuser名"

    mock_cognito.admin_get_user.return_value = {
        # Cognitoのadmin_get_userメソッドが返すべき値を設定
        ...
    }
    # get_user_attributes関数を呼び出す
    response = get_user_attributes(username)
    assert response == {
         # 戻り値が期待通りであることを確認
        ...
    }
    # admin_get_userが正しい引数で一度だけ呼ばれたことを確認
    mock_cognito.admin_get_user.assert_called_once_with(
        UserPoolId="dummy_pool_id", Username=username)

解析の補足:pythonのコード実行順が原因

Pythonでは、モジュールがインポートされるとき、そのスクリプト内のトップレベルのコードが上から下へと順番に実行されます。このトップレベルのコードとは、関数やクラスの外部で直接書かれたコードを指します。関数やクラスの内部のコードはその関数やクラスが呼び出されるまで実行されません。

なので、モックの設定よりも前にAWS Cognitoのクライアントが初期化されてしまい、結果としてテスト中に実際のCognitoクライアントが使用されてしまいます。

解決策: AWSクライアント初期化の位置を調整

この問題を解決するために、AWSクライアントの初期化をテスト対象関数の実行前に行い、関数の引数として渡すようにコードを修正しました。具体的には以下です。

テスト対象関数

def get_user_attributes(username, cognito):
    cognito_user_pool_id = os.environ['USER_POOL_ID']
    return cognito.admin_get_user(
        UserPoolId=cognito_user_pool_id,
        Username=username
    )

pytestのコード

@patch("boto3.client")
def test_get_user_attributes(mock_client):
    mock_cognito = MagicMock()
    mock_client.return_value = mock_cognito
    os.environ["USER_POOL_ID"] = "dummy_pool_id"
    username = "検証に使用するuser名"

    mock_cognito.admin_get_user.return_value = {
        # Cognitoのadmin_get_userメソッドが返すべき値を設定
        ...
    }
    # get_user_attributes関数を呼び出す
    response = get_user_attributes(username, mock_cognito)
    assert response == {
         # 戻り値が期待通りであることを確認
        ...
    }
    # admin_get_userが正しい引数で一度だけ呼ばれたことを確認
    mock_cognito.admin_get_user.assert_called_once_with(
        UserPoolId="dummy_pool_id", Username=username)

これにより、テスト時にはモック化されたclientが使用され、エラーが解消されました。

まとめ

  • AWSのCognitoサービスのadmin_get_user APIをテストする際にNoCredentialsErrorというエラーが発生した。
  • エラーの原因は、テスト実行時にモック化する前に実際のboto3.client('cognito-idp')が実行されてしまうことで、実際のAWS認証情報を探しに行ってしまい、情報が見つからないためだった。
  • Pythonのコードの実行順と、モジュールのインポート時の挙動を理解することが、この種の問題の解決の鍵となる。
  • 解決策として、AWSクライアントの初期化をテスト対象関数の実行前に行い、関数の引数として渡すようにコードを修正した。

今回の経験を通じて、テストを書く際の注意点やPythonの特性について改めて学びました。特に外部サービスとの連携がある場面でのテストは慎重に行う必要があることを感じました。モック化の方法やテストの流れをしっかり理解し、正確なテストを実行することで、安全かつ効率的な開発を進めることができます。

最後までお読みいただき、ありがとうございました。同じような問題に直面している方の助けになれば幸いです。

AUTHOR
champ
champ
記事URLをコピーしました