web-dev-qa-db-ja.com

boto3 S3クライアントメソッドのモックPython

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メソッドでのみ例外をスローする方法についてのアイデアはありますか?

46
ptimson

ここに投稿するとすぐに、解決策を思いつきました。ここでそれが役立つことを願っています:)

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 クラスを使用します。よりクリーンなソリューションでありながら、特定の操作をモックすることはできませんでした。

17
ptimson

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
66
Jordon Phillips

以下は、偽の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'
}

うまくいけばそれが助けになる。

6
Aidan Melen

moto を使用するだけではどうですか?

非常に便利です decorator

from moto import mock_s3

@mock_s3
def test_my_model_save():
    pass
4
wikier

いくつかの統合テストのためにboto3クライアントをモックする必要があり、少し苦痛でした!私が抱えていた問題は、motoKMSを十分にサポートしていないにもかかわらず、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のコードは省略しました。テスト中の機能と同様に。これらのfoobar関数を呼び出しましょう。

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'

私はそれがおそらく最も美しいコードではないことを知っていますが、普遍的な何かを探しているなら、それはかなりうまくいくはずです!

3
Barmaley

プロジェクトの腸で使用されるボトクライアントに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エントリとして指定して、すべてのテスト実行で使用されるようにします。

1
deargle

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)

0
c4urself