Пересмешивать несколько сервисов boto3, некоторые без реализации moto - PullRequest
0 голосов
/ 18 апреля 2020

Я пытаюсь провести модульное тестирование logi c в функции AWS Lambda с помощью насмешек. Лямбда завершает свое выполнение, отправляя уведомления pu sh через AWS Pinpoint. Lambda также использует AWS SSM Parameter Store. Я издевался над другими Lambdas, с несколькими объектами boto3, с moto https://github.com/spulec/moto, но в настоящее время в moto нет реализации Pinpoint.

Я нашел решение в { ссылка }, которое мне нужно было изменить, чтобы оно заработало. Вопрос, на который он отвечал, был не о моем точном сценарии, но ответ указал мне на решение. Поэтому я публикую здесь, чтобы задокументировать мои изменения в решении, которое я модифицировал, и спросить, есть ли более элегантный способ сделать это. Я посмотрел на botocore.stub.Stubber, но не вижу, как он лучше, но я готов ошибиться.

Пока мой код:

test.py

import unittest
from unittest.mock import MagicMock, patch
import boto3
from moto import mock_ssm
import my_module


def mock_boto3_client(*args, **kwargs):
    if args[0] == 'ssm':
        # Use moto.
        mock_client = boto3.client(*args, **kwargs)
    else:
        mock_client = boto3.client(*args, **kwargs)
        if args[0] == 'pinpoint':
            # Use MagicMock.
            mock_client.create_segment = MagicMock(
                return_value={'SegmentResponse': {'Id': 'Mock SegmentID'}}
            )
            mock_client.create_campaign = MagicMock(
                return_value={'response': 'Mock Response'}
            )
    return mock_client


class TestMyModule(unittest.TestCase):
    @patch('my_module.boto3')
    @mock_ssm
    def test_my_module(self, mock_boto3):
        mock_boto3.client = mock_boto3_client
        conn = mock_boto3.client('ssm', region_name='eu-west-2')
        conn.put_parameter(
            Name='/my/test',
            Value="0123456789",
            Type='String',
            Tier='Standard'
        )
        response = my_module.handler()
        self.assertEqual(
            ('0123456789', 'Mock SegmentID', {'response': 'Mock Response'}), 
            response
        )

my_module.py

import boto3
import json


def get_parameter():
    ssm = boto3.client('ssm', region_name='eu-west-2')
    parameter = ssm.get_parameter(Name='/my/test')
    return parameter['Parameter']['Value']


def create_segment(client, message_id, push_tags, application_id):
    response = client.create_segment(
        ApplicationId=application_id,
        WriteSegmentRequest={
            'Dimensions': {
                'Attributes': {
                    'pushTags': {
                        'AttributeType': 'INCLUSIVE',
                        'Values': push_tags
                    }
                }
            },
            'Name': f'Segment {message_id}'
        }
    )
    return response['SegmentResponse']['Id']


def create_campaign(client, message_id, segment_id, application_id):
    message_payload_apns = json.dumps({
        "aps": {
            "alert": 'My Alert'
        },
        "messageId": message_id,
    })

    response = client.create_campaign(
        ApplicationId=application_id,
        WriteCampaignRequest={
            'Description': f'Test campaign - message {message_id} issued',
            'MessageConfiguration': {
                'APNSMessage': {
                    'Action': 'OPEN_APP',
                    'RawContent': message_payload_apns
                }
            },
            'Name': f'{message_id} issued',
            'Schedule': {
                'StartTime': 'IMMEDIATE'
            },
            'SegmentId': segment_id
        }
    )
    return response


def handler():
    application_id = get_parameter()
    client = boto3.client('pinpoint', region_name='eu-west-1')
    segment_id = create_segment(client, 12345, [1, 2], application_id)
    response = create_campaign(client, 12345, segment_id, application_id)
    return application_id, segment_id, response

В частности, я хотел бы знать, как лучше и элегантнее реализовать mock_boto3_client () для обработки более обобщенным c способом.

Ответы [ 2 ]

1 голос
/ 25 апреля 2020

Как я сказал в своем комментарии в ответ на ответ Берта Бломмерса

"Мне удалось зарегистрировать дополнительный сервис в Moto-framework для pinpoint create_app (), но не удалось реализовать create_segment () как botocore получает "locationName": "идентификатор приложения" из botocore / data / pinpoint / 2016-12-01 / service-2. json, а затем moto \ core \ response.py пытается сделать с ним регулярное выражение, но создает ' / v1 / apps / {application-id} / сегменты 'с недопустимым дефисом "

Но я опубликую здесь свой рабочий код для create_app () для других людей, которые читают this post.

Структура пакета важна тем, что пакет "pinpoint" должен находиться в одном другом пакете.

.
├── mock_pinpoint
│   └── pinpoint
│       ├── __init__.py
│       ├── pinpoint_models.py
│       ├── pinpoint_responses.py
│       └── pinpoint_urls.py
├── my_module.py
└── test.py

mock_pinpoint / pinpoint / init . py

from __future__ import unicode_literals
from mock_pinpoint.pinpoint.pinpoint_models import pinpoint_backends
from moto.core.models import base_decorator

mock_pinpoint = base_decorator(pinpoint_backends)

mock_pinpoint / pinpoint / pinpoint_models.py

from boto3 import Session
from moto.core import BaseBackend


class PinPointBackend(BaseBackend):

    def __init__(self, region_name=None):
        self.region_name = region_name

    def create_app(self):
        # Store the app in memory, to retrieve later
        pass


pinpoint_backends = {}
for region in Session().get_available_regions("pinpoint"):
    pinpoint_backends[region] = PinPointBackend(region)

mock_pinpoint / pinpoint / pinpoint_responses.py

from __future__ import unicode_literals
import json
from moto.core.responses import BaseResponse
from mock_pinpoint.pinpoint import pinpoint_backends


class PinPointResponse(BaseResponse):
    SERVICE_NAME = "pinpoint"

    @property
    def pinpoint_backend(self):
        return pinpoint_backends[self.region]

    def create_app(self):
        body = json.loads(self.body)
        response = {
            "Arn": "arn:aws:mobiletargeting:eu-west-1:AIDACKCEVSQ6C2EXAMPLE:apps/810c7aab86d42fb2b56c8c966example",
            "Id": "810c7aab86d42fb2b56c8c966example",
            "Name": body['Name'],
            "tags": body['tags']
        }
        return 200, {}, json.dumps(response)

mock_pinpoint / pinpoint / pinpoint_urls.py

from __future__ import unicode_literals
from .pinpoint_responses import PinPointResponse

url_bases = ["https?://pinpoint.(.+).amazonaws.com"]
url_paths = {"{0}/v1/apps$": PinPointResponse.dispatch}

my_module.py

* 102 7 *

test.py

import unittest
import boto3
from moto import mock_ssm
import my_module
from mock_pinpoint.pinpoint import mock_pinpoint


class TestMyModule(unittest.TestCase):
    @mock_pinpoint
    @mock_ssm
    def test_my_module(self):
        conn = boto3.client('ssm', region_name='eu-west-2')
        conn.put_parameter(
            Name='/my/test',
            Value="0123456789",
            Type='String',
            Tier='Standard'
        )
        application_id, app = my_module.handler()
        self.assertEqual('0123456789', application_id)
        self.assertEqual(
            'arn:aws:mobiletargeting:eu-west-1:AIDACKCEVSQ6C2EXAMPLE:apps/810c7aab86d42fb2b56c8c966example',
            app['ApplicationResponse']['Arn']
        )
        self.assertEqual(
            '810c7aab86d42fb2b56c8c966example',
            app['ApplicationResponse']['Id']
        )
        self.assertEqual(
            'my_app',
            app['ApplicationResponse']['Name']
        )
        self.assertEqual(
            {"my_tag": "tag"},
            app['ApplicationResponse']['tags']
        )

Сказав, что решение исходного вопроса работает и его легче реализовать, но не так элегантно.

1 голос
/ 20 апреля 2020

Сравнительно легко использовать moto framework для любых новых сервисов. Это позволяет вам сосредоточиться на требуемом поведении, а moto заботится о строительных лесах.

Для регистрации дополнительной услуги в Moto-framework требуется два шага:

  1. Убедитесь это moto проверяет фактические HTTP-запросы на https://pinpoint.aws.amazon.com
  2. Создать класс Responses, который действует на запросы для https://pinpoint.aws.amazon.com

Дразнить реальные HTTP-запросы можно, расширив BaseBackend-класс из moto. Обратите внимание на URL-адреса и тот факт, что все запросы к этому URL-адресу будут проверяться классом PinPointResponse.

pinpoint_mock / models.py :

import re

from boto3 import Session

from moto.core import BaseBackend
from moto.sts.models import ACCOUNT_ID



class PinPointBackend(BaseBackend):

    def __init__(self, region_name):
        self.region_name = region_name

    @property
    def url_paths(self):
        return {"{0}/$": PinPointResponse.dispatch}

    @property
    def url_bases(self):
        return ["https?://pinpoint.(.+).amazonaws.com"]

    def create_app(self, name):
        # Store the app in memory, to retrieve later
        pass


pinpoint_backends = {}
for region in Session().get_available_regions("pinpoint"):
    pinpoint_backends[region] = PinPointBackend(region)
for region in Session().get_available_regions(
    "pinpoint", partition_name="aws-us-gov"
):
    pinpoint_backends[region] = PinPointBackend(region)
for region in Session().get_available_regions("pinpoint", partition_name="aws-cn"):
    pinpoint_backends[region] = PinPointBackend(region)

Класс Response должен расширять класс BaseResponse из moto и дублировать имена методов, которые вы пытаетесь смоделировать.
pinpoint / response.py

from __future__ import unicode_literals

import json

from moto.core.responses import BaseResponse
from moto.core.utils import amzn_request_id
from .models import pinpoint_backends


class PinPointResponse(BaseResponse):
    @property
    def pinpoint_backend(self):
        return pinpoint_backends[self.region]

    @amzn_request_id
    def create_app(self):
        name = self._get_param("name")
        pinpoint_backend.create_app(name)
        return 200, {}, {}

Теперь все, что осталось, - это создать декоратор:

from __future__ import unicode_literals
from .models import stepfunction_backends
from ..core.models import base_decorator

pinpoint_backend = pinpoint_backends["us-east-1"]
mock_pinpoint = base_decorator(pinpoint_backends)

@mock_pinpoint
def test():
    client = boto3.client('pinpoint')
    client.create_app(Name='testapp')

Код был взят из модуля StepFunctions, который, вероятно, является одним из самых простых модулей и его проще всего адаптировать к вашим потребностям: https://github.com/spulec/moto/tree/master/moto/stepfunctions

...