Boto3 s3クライアントオブジェクトから単一のメソッドをモックして例外をスローしようとしています。しかし、このクラスが通常どおり動作するためには、他のすべてのメソッドが必要です。
これは、 pload_part_copy の実行時にエラーが発生したときに、単一の例外テストをテストできるようにするためです。
最初の試み
import boto3
from mock import patch
with patch('botocore.client.S3.upload_part_copy', side_effect=Exception('Error Uploading')) as mock:
client = boto3.client('s3')
# Should return actual result
o = client.get_object(Bucket='my-bucket', Key='my-key')
# Should return mocked exception
e = client.upload_part_copy()
ただし、これにより次のエラーが発生します。
ImportError: No module named S3
2回目の試行
Botocore.client.pyのソースコードを確認したところ、巧妙な処理が行われており、メソッドupload_part_copy
が存在しないことがわかりました。代わりにBaseClient._make_api_call
を呼び出すように見えるので、それをモックしようとしました
import boto3
from mock import patch
with patch('botocore.client.BaseClient._make_api_call', side_effect=Exception('Error Uploading')) as mock:
client = boto3.client('s3')
# Should return actual result
o = client.get_object(Bucket='my-bucket', Key='my-key')
# Should return mocked exception
e = client.upload_part_copy()
これは例外をスローします...しかし、get_object
で私は避けたいです。
upload_part_copy
メソッドでのみ例外をスローする方法についてのアイデアはありますか?
ここに投稿するとすぐに、解決策を思いつきました。ここでそれが役立つことを願っています:)
import botocore
from botocore.exceptions import ClientError
from mock import patch
import boto3
orig = botocore.client.BaseClient._make_api_call
def mock_make_api_call(self, operation_name, kwarg):
if operation_name == 'UploadPartCopy':
parsed_response = {'Error': {'Code': '500', 'Message': 'Error Uploading'}}
raise ClientError(parsed_response, operation_name)
return orig(self, operation_name, kwarg)
with patch('botocore.client.BaseClient._make_api_call', new=mock_make_api_call):
client = boto3.client('s3')
# Should return actual result
o = client.get_object(Bucket='my-bucket', Key='my-key')
# Should return mocked exception
e = client.upload_part_copy()
Jordan Philipsも素晴らしいソリューションを投稿しましたbotocore.stub.Stubber クラスを使用します。よりクリーンなソリューションでありながら、特定の操作をモックすることはできませんでした。
Botocoreには、この目的だけに使用できるクライアントスタブがあります: docs 。
エラーを入れる例を次に示します。
import boto3
from botocore.stub import Stubber
client = boto3.client('s3')
stubber = Stubber(client)
stubber.add_client_error('upload_part_copy')
stubber.activate()
# Will raise a ClientError
client.upload_part_copy()
次に、通常の応答を入力する例を示します。さらに、コンテキストでスタブを使用できるようになりました。できる限り、提供された応答がサービスが実際に返すものと一致することを、スタブが検証することに注意することが重要です。これは完全ではありませんが、完全な無意味な応答を挿入することから保護します。
import boto3
from botocore.stub import Stubber
client = boto3.client('s3')
stubber = Stubber(client)
list_buckets_response = {
"Owner": {
"DisplayName": "name",
"ID": "EXAMPLE123"
},
"Buckets": [{
"CreationDate": "2016-05-25T16:55:48.000Z",
"Name": "foo"
}]
}
expected_params = {}
stubber.add_response('list_buckets', list_buckets_response, expected_params)
with stubber:
response = client.list_buckets()
assert response == list_buckets_response
以下は、偽のclient = boto3.client( 'ec2') api call ...に使用できる単純なpython unittestの例です。
import boto3
class MyAWSModule():
def __init__(self):
client = boto3.client('ec2')
tags = client.describe_tags(DryRun=False)
class TestMyAWSModule(unittest.TestCase):
@mock.patch("boto3.client.get_tags")
@mock.patch("boto3.client")
def test_open_file_with_existing_file(self, mock_boto_client, mock_describe_tags):
mock_boto_client.return_value = mock_get_tags_response
my_aws_module = MyAWSModule()
mock_boto_client.assert_call_once('ec2')
mock_describe_tags.assert_call_once_with(DryRun=False)
mock_get_tags_response = {
'Tags': [
{
'ResourceId': 'string',
'ResourceType': 'customer-gateway',
'Key': 'string',
'Value': 'string'
},
],
'NextToken': 'string'
}
うまくいけばそれが助けになる。
いくつかの統合テストのためにboto3
クライアントをモックする必要があり、少し苦痛でした!私が抱えていた問題は、moto
がKMS
を十分にサポートしていないにもかかわらず、S3
バケット用に自分のモックを書き直したくなかったことです。だから私はすべての答えのこのモーフを作成しました。また、それはグローバルに動作し、かなりクールです!
2つのファイルでセットアップしています。
最初はaws_mock.py
です。 KMS
モックについては、ライブboto3
クライアントからの事前定義済みの応答をいくつか取得しました。
from unittest.mock import MagicMock
import boto3
from moto import mock_s3
# `create_key` response
create_resp = { ... }
# `generate_data_key` response
generate_resp = { ... }
# `decrypt` response
decrypt_resp = { ... }
def client(*args, **kwargs):
if args[0] == 's3':
s3_mock = mock_s3()
s3_mock.start()
mock_client = boto3.client(*args, **kwargs)
else:
mock_client = boto3.client(*args, **kwargs)
if args[0] == 'kms':
mock_client.create_key = MagicMock(return_value=create_resp)
mock_client.generate_data_key = MagicMock(return_value=generate_resp)
mock_client.decrypt = MagicMock(return_value=decrypt_resp)
return mock_client
2つ目は、実際のテストモジュールです。 test_my_module.py
と呼びましょう。 my_module
のコードは省略しました。テスト中の機能と同様に。これらのfoo
、bar
関数を呼び出しましょう。
from unittest.mock import patch
import aws_mock
import my_module
@patch('my_module.boto3')
def test_my_module(boto3):
# Some prep work for the mock mode
boto3.client = aws_mock.client
conn = boto3.client('s3')
conn.create_bucket(Bucket='my-bucket')
# Actual testing
resp = my_module.foo()
assert(resp == 'Valid')
resp = my_module.bar()
assert(resp != 'Not Valid')
# Etc, etc, etc...
もう1つ、それが修正されるかどうかはわかりませんが、資格情報や地域などの環境変数を設定しない限り、moto
は満足できないことがわかりました。実際の資格情報である必要はありませんが、設定する必要があります。これを読むまでに修正される可能性があります!しかし、必要な場合に備えて、今回はシェルコードを使用します。
export AWS_ACCESS_KEY_ID='foo'
export AWS_SECRET_ACCESS_KEY='bar'
export AWS_DEFAULT_REGION='us-east-1'
私はそれがおそらく最も美しいコードではないことを知っていますが、普遍的な何かを探しているなら、それはかなりうまくいくはずです!
プロジェクトの腸で使用されるボトクライアントにpytest
フィクスチャをパッチするための私のソリューションを次に示します。私のプロジェクトでは「mturk」のみを使用しています。
私にとっての秘trickは、自分のクライアントを作成し、その事前作成されたクライアントを返す関数でboto3.client
にパッチを当てることでした。
@pytest.fixture(scope='session')
def patched_boto_client():
my_client = boto3.client('mturk')
def my_client_func(*args, **kwargs):
return my_client
with patch('bowels.of.project.other_module.boto3.client', my_client_func):
yield my_client_func
def test_create_hit(patched_boto_client):
client = patched_boto_client()
stubber = Stubber(client)
stubber.add_response('create_hit_type', {'my_response':'is_great'})
stubber.add_response('create_hit_with_hit_type', {'my_other_response':'is_greater'})
stubber.activate()
import bowels.of.project # this module imports `other_module`
bowels.of.project.create_hit_function_that_calls_a_function_in_other_module_which_invokes_boto3_dot_client_at_some_point()
また、ダミーのaws credsをセットアップする別のフィクスチャを定義して、botoがシステム上の他のクレデンシャルセットを誤って取得しないようにします。私は文字通り「foo」と「bar」をテスト用のクレデンシャルとして設定していますが、それは編集ではありません。
AWS_PROFILE
envの設定を解除することが重要です。そうしないと、botoはそのプロファイルを探しに行きます。
@pytest.fixture(scope='session')
def setup_env():
os.environ['AWS_ACCESS_KEY_ID'] = 'foo'
os.environ['AWS_SECRET_ACCESS_KEY'] = 'bar'
os.environ.pop('AWS_PROFILE', None)
そして、setup_env
をpytest usefixtures
エントリとして指定して、すべてのテスト実行で使用されるようにします。
moto
またはbotocoreスタブのいずれかを使用したくない場合(スタブはnot AWS APIエンドポイントへのHTTPリクエストの発生を防ぐようです)、より詳細なunittestを使用できます.mock方法:
foo/bar.py
import boto3
def my_bar_function():
client = boto3.client('s3')
buckets = client.list_buckets()
...
bar_test.py
import unittest
from unittest import mock
class MyTest(unittest.TestCase):
@mock.patch('foo.bar.boto3.client')
def test_that_bar_works(self, mock_s3_client):
self.assertTrue(mock_s3_client.return_value.list_buckets.call_count == 1)