模拟boto3 S3客户端方法Python


78

我正在尝试从boto3 s3客户端对象模拟一个单独的方法来引发异常。但是我需要所有其他方法来使此类正常工作。

这样一来,我可以在执行upload_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

第二次尝试

查看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方法上引发异常的任何想法?

Answers:


111

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()

这是放置正常响应的示例。此外,现在可以在上下文中使用该Stubber。重要的是要注意,存根将在可能的范围内验证您提供的响应与服务将实际返回的内容匹配。这不是完美的方法,但是它将保护您避免插入总的废话响应。

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

1
好吧,由于它位于botocore中,因此您必须查看botocore文档,而这样做的人并不多。这也是最近的事。
乔登·菲利普斯

2
为什么client.upload_part_copy()会引发ClientError?
艾丹·梅伦

1
@AidanMelen,因为我向响应队列中明确添加了一个错误。您还可以添加常规服务响应。我将更新以同时显示两者。
乔登·菲利普斯

8
是否client需要注入被测单元?我对Pythonic单元测试的理解是,测试人员使用类似unittest.mock模拟导入依赖项的方法。这种方法会模拟其他文件中导入的boto客户端吗?
卡尔·G

您将以与使用模拟对象相同的方式使用这些对象。您设置它们,然后将它们传递到您的代码中。如何传递它们取决于您。
乔丹·菲利普斯

42

一旦我在这里发布,我就设法提出了一个解决方案。希望对您有帮助:)

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发布了一个不错的解决方案。虽然使用更干净的解决方案,但我无法模拟特定的操作。


4
这非常有帮助。我花了一段时间才意识到很多boto3客户端是在运行时有效生成的,因此不能直接对其进行模拟。
rumdrums

1
这是适用于我的解决方案,因为Stubber和许多其他模拟工具无法对boto3定制功能(例如上传文件或生成的预签名URL)进行存根。
JD D

1
这个答案很好。我首先尝试使用Stubber,但它似乎只能用于立即调用,出于某种原因,我无法使其在子函数中的调用中起作用。另一方面,这可以完美工作并且非常容易实现,所以谢谢!
杰克·邦加登

如何模拟该get_object函数,当我尝试运行上述代码时,get对象调用不会通过模拟。
Bhavani Ravi

11

这是一个简单的python unittest的示例,可用于伪造client = boto3.client('ec2') api调用...

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_describe_tags.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'
}

希望有帮助。


如何管理全局客户端或资源对象?这不能被模拟,因为它的调用发生在模拟设置之前。
pt12lol

3
“ test_open_file_with_existing_file”的第一行不应为“ mock_describe_tags.return_value = mock_get_tags_response”吗?而不是'mock_boto_client'吗?
cloud_weather

您如何模拟@ mock.patch(“ boto3.client.get_tags”)
Shivangi Singh

1
这篇文章很好。如果您还有其他问题,请参考此内容。toptal.com/python/an-introduction-to-mocking-in-python
艾丹·梅伦

7

那么简单地使用moto呢?

它带有一个非常方便的装饰器

from moto import mock_s3

@mock_s3
def test_my_model_save():
    pass

7

如果您既不想使用motoBotocore存根,也可以使用Botocore存根(该存根不会阻止对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)


4

我不得不模拟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

第二个是实际的测试模块。叫它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...

还有一件事,不确定是否固定,但我发现moto除非您设置一些环境变量(例如凭据和区域),否则这是不愉快的。它们不必是实际的凭证,但是必须进行设置。阅读本文时,它可能会修复!但是这里有一些代码,如果您确实需要它,这次是shell代码!

export AWS_ACCESS_KEY_ID='foo'
export AWS_SECRET_ACCESS_KEY='bar'
export AWS_DEFAULT_REGION='us-east-1'

我知道这可能不是最漂亮的代码,但是如果您正在寻找通用的东西,它应该会很好地工作!


这与我自己的用例非常接近-我必须处理来自boto3而不是KMS的组织调用。但是-因为所有很酷的孩子现在都在使用它-我正在尝试使用pytest(和pytest-mock),但无法将其客户端功能修补到MagicMock中。您是否尝试过使用pytest而不是unittest?注意:我本人最近才从unittest切换到pytest仍然有点困惑。
Marakai

更新:从字面上看,它可以很好地与Pytest配合使用。当我有这个稳定器时,可以在上面发布答案。
Marakai

@Marakai,我实际上是在使用pytest来运行测试。我猜我对单元测试有些陌生,但没有意识到pytest有自己的模拟实现。希望实施起来并不太难!
巴马利

1
如果我理解正确(并且我对此不是专家),则pytest中的模拟框架只是unittest模拟框架的包装。我发现,我可以使用@pytest.fixture,并@mock.patch和它的作品。我确实希望我能不止一次地对您的回答表示支持,这对我能够持续使用boto3存根(即使对于那些尚不支持的客户)也有很大帮助。
马拉凯'19

@Marakai,很高兴您发现我的帖子有用!我也很高兴我可以为堆栈溢出社区做出贡献!
Barmaley '19

3

这是我的解决方案,用于修补项目肠子中使用的boto客户端, pytest夹具。我在我的项目中只使用“ mturk”。

对我来说,诀窍是创建自己的客户端,然后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凭据的固定装置,以便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为pytestusefixtures条目,以便在每次测试运行中都使用它。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.