1

给定如下代码:

def do_stuff():

    session = boto3.session.Session()
    client = session.client(service_name="secretsmanager", region_name=region_name)
    client.get_secret_value(SecretId=sendgrid_api_key_arn)

我如何模拟 client.get_secret_value("some-value")返回一些值

以及如何模拟它以引发异常

@patch("boto3.session")
def test_get_sendgrid_api_key_secret_when_client_error(mock_session):
        session = mock_session.Session();
        client = session.client()
        client.get_secret_value().return_value = 
                 {"SecretString": "my-secret"} <- this is wrapped in a MagicMock which is useless.
4

3 回答 3

0

您需要client.get_secret_value通过添加一个装饰器来进行修补。完成此操作后,您可以将 return_value 设置为您需要的异常。

@patch("boto3.session")
@patch("boto3.session.Session.client.get_secret_value")
def test_get_sendgrid_api_key_secret_when_client_error(mock_session,second_fn):
        with pytest.raises('CustomException')
        session = mock_session.Session();
        second_fn.return_value = lambda : Exception('CustomException')
        client = session.client()
        client.get_secret_value()

执行时client.get_secret_value(),测试用例将通过以引发预期的异常。类似地,我们可以分配任何其他值来模拟函数的返回,而不是 lambda。

于 2021-05-12T12:15:10.377 回答
0

您需要设置一个返回值树:

  • boto3.session.Session需要返回一个模拟对象
  • 该模拟对象需要一个client返回另一个模拟对象的方法
  • 那个模拟对象需要一个get_secret_value返回假值的方法

如果我假设它target.py存在并包含:

import boto3.session


def do_stuff():
    session = boto3.session.Session()
    client = session.client(service_name="secretsmanager", region_name='myregion')
    return client.get_secret_value(SecretId='some-secret-id')

然后我可以像这样测试它:

from unittest import mock

import target


@mock.patch("boto3.session.Session")
def test_do_stuff(mock_session_class):
    mock_session_object = mock.Mock()
    mock_client = mock.Mock()
    mock_client.get_secret_value.return_value = {'SecretString': 'my-secret'}
    mock_session_object.client.return_value = mock_client
    mock_session_class.return_value = mock_session_object

    res = target.do_stuff()
    assert res['SecretString'] == 'my-secret'

这行得通,尽管我怀疑有一种更优雅的方式来设置它。

于 2021-05-12T12:02:48.330 回答
0

手动修补的一个问题boto3.session是,如果您使用除 之外的其他 AWS 服务secretsmanager,那么模拟的补丁将应用于所有内容,这可能不是您想要的。

我在这里提出 2 个专门用于修补/包装 secretsmanager 功能的解决方案:

  1. 请参阅下面的./test_secret_manual_amend.py。拦截botocore.client.BaseClient._make_api_call.GetSecretValue并控制响应的方式。这是更灵活的解决方案,因为您可以自由检查访问的秘密名称、引发异常并返回您喜欢的任何内容。
  2. 请参阅下面的./test_secret_using_moto.py。使用moto.mock_secretsmanager. 这是最简洁的方法,因为它模仿了您通常与实际 AWs secretsmanager 交互的方式。但可能不如解决方案 1 灵活。

./src.py

import boto3

sendgrid_api_key_arn = "some_name"
region_name = "ap-southeast-1"
credentials = {}


def do_stuff():
    session = boto3.session.Session(**credentials)
    client = session.client(service_name="secretsmanager", region_name=region_name)
    return client.get_secret_value(SecretId=sendgrid_api_key_arn)

./test_secret_manual_amend.py

import botocore
import json
import pytest

from src import do_stuff, sendgrid_api_key_arn


def _amend_get_secret_value(secret_name, secret_value, mocker):
    orig = botocore.client.BaseClient._make_api_call

    def amend_make_api_call(self, operation_name, kwargs):
        # Intercept boto3 operations for <secretsmanager.get_secret_value>. Optionally, you can also
        # check on the argument <SecretId> and control how you want the response would be. This is
        # a very flexible solution as you have full control over the whole process of fetching a
        # secret.
        if operation_name == 'GetSecretValue' and kwargs["SecretId"] == secret_name:
            if isinstance(secret_value, Exception):
                raise secret_value
            return {
                'Name': secret_name,
                'SecretString': secret_value,
            }

        return orig(self, operation_name, kwargs)

    mocker.patch('botocore.client.BaseClient._make_api_call', new=amend_make_api_call)


@pytest.mark.parametrize(
    'secret_value',
    [
        "some value",
        str(1993),
        json.dumps({"SecretString": "my-secret"}),
        json.dumps([2, 3, 5, 7, 11, 13, 17, 19]),
        KeyError("How dare you touch my secret!"),
        ValueError("Oh my goodness you even have the guts to repeat it!!!"),
    ],
)
def test_secret_manual_amend(secret_value, mocker):
    _amend_get_secret_value(sendgrid_api_key_arn, secret_value, mocker)

    if isinstance(secret_value, Exception):
        with pytest.raises(type(secret_value)) as error:
            do_stuff()
        result = error
    else:
        result = do_stuff()

    print("Result:", result)

./test_secret_using_moto.py

import boto3
import json
from moto import mock_secretsmanager
import pytest

from src import do_stuff, sendgrid_api_key_arn


def _setup_secrets_manager(secret_name, secret_value):
    secret_manager = boto3.client('secretsmanager')
    secret_manager.create_secret(
        Name=secret_name,
        SecretString=secret_value,
    )


@mock_secretsmanager
@pytest.mark.parametrize(
    'secret_value',
    [
        "some value",
        str(1993),
        json.dumps({"SecretString": "my-secret"}),
        json.dumps([2, 3, 5, 7, 11, 13, 17, 19]),
    ],
)
def test_secret_using_moto(secret_value):
    _setup_secrets_manager(sendgrid_api_key_arn, secret_value)

    result = do_stuff()

    print("Result:", result)

输出:

$ pytest -rP
====================================================================================== test session starts ======================================================================================
test_secret_manual_amend.py ......                                                                                                                                                        [ 60%]
test_secret_using_moto.py ....                                                                                                                                                            [100%]

============================================================================================ PASSES =============================================================================================


_____________________________________________________________________________ test_secret_manual_amend[some value] ______________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: {'Name': 'some_name', 'SecretString': 'some value'}
________________________________________________________________________________ test_secret_manual_amend[1993] _________________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: {'Name': 'some_name', 'SecretString': '1993'}
____________________________________________________________________ test_secret_manual_amend[{"SecretString": "my-secret"}] ____________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: {'Name': 'some_name', 'SecretString': '{"SecretString": "my-secret"}'}
____________________________________________________________________ test_secret_manual_amend[[2, 3, 5, 7, 11, 13, 17, 19]] _____________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: {'Name': 'some_name', 'SecretString': '[2, 3, 5, 7, 11, 13, 17, 19]'}
____________________________________________________________________________ test_secret_manual_amend[secret_value4] ____________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: <ExceptionInfo KeyError('How dare you touch my secret!') tblen=4>
____________________________________________________________________________ test_secret_manual_amend[secret_value5] ____________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: <ExceptionInfo ValueError('Oh my goodness you even have the guts to repeat it!!!') tblen=4>


______________________________________________________________________________ test_secret_using_moto[some value] _______________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: {'ARN': 'arn:aws:secretsmanager:ap-southeast-1:1234567890:secret:some_name-ghtZn', 'Name': 'some_name', 'VersionId': '42a45aad-8764-49fb-b1ad-67759063f804', 'SecretString': 'some value', 'VersionStages': ['AWSCURRENT'], 'CreatedDate': datetime.datetime(2021, 5, 13, 15, 3, 43, tzinfo=tzlocal()), 'ResponseMetadata': {'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com'}, 'RetryAttempts': 0}}
_________________________________________________________________________________ test_secret_using_moto[1993] __________________________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: {'ARN': 'arn:aws:secretsmanager:ap-southeast-1:1234567890:secret:some_name-JdOXC', 'Name': 'some_name', 'VersionId': '098b2878-4368-4f25-b75d-942e82745257', 'SecretString': '1993', 'VersionStages': ['AWSCURRENT'], 'CreatedDate': datetime.datetime(2021, 5, 13, 15, 3, 44, tzinfo=tzlocal()), 'ResponseMetadata': {'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com'}, 'RetryAttempts': 0}}
_____________________________________________________________________ test_secret_using_moto[{"SecretString": "my-secret"}] _____________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: {'ARN': 'arn:aws:secretsmanager:ap-southeast-1:1234567890:secret:some_name-jQgDK', 'Name': 'some_name', 'VersionId': '04bd6ceb-6ec9-427b-817c-c90360abfcd9', 'SecretString': '{"SecretString": "my-secret"}', 'VersionStages': ['AWSCURRENT'], 'CreatedDate': datetime.datetime(2021, 5, 13, 15, 3, 44, tzinfo=tzlocal()), 'ResponseMetadata': {'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com'}, 'RetryAttempts': 0}}
_____________________________________________________________________ test_secret_using_moto[[2, 3, 5, 7, 11, 13, 17, 19]] ______________________________________________________________________
------------------------------------------------------------------------------------- Captured stdout call --------------------------------------------------------------------------------------
Result: {'ARN': 'arn:aws:secretsmanager:ap-southeast-1:1234567890:secret:some_name-dQpLq', 'Name': 'some_name', 'VersionId': '94b7a169-c32b-4424-b47c-2dee2802c211', 'SecretString': '[2, 3, 5, 7, 11, 13, 17, 19]', 'VersionStages': ['AWSCURRENT'], 'CreatedDate': datetime.datetime(2021, 5, 13, 15, 3, 44, tzinfo=tzlocal()), 'ResponseMetadata': {'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com'}, 'RetryAttempts': 0}}
====================================================================================== 10 passed in 1.26s =======================================================================================
于 2021-05-13T07:10:13.780 回答