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

Я нашел решение в https://stackoverflow.com/a/55527212/839338, которое мне нужно было изменить, чтобы получить его работать. Вопрос, на который он отвечал, был не о моем точном сценарии, но ответ указал мне на решение. Поэтому я публикую здесь, чтобы задокументировать мои изменения в решении, которое я модифицировал, и спросить, есть ли более элегантный способ сделать это. Я посмотрел на 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 () для обработки более общим способом.

1
Dan-Dev 18 Апр 2020 в 23:11

2 ответа

Лучший ответ

Относительно легко использовать мото-фреймворк для любых новых сервисов. Это позволяет вам сосредоточиться на требуемом поведении, а мото заботится о строительных лесах.

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

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

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

< EM> 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 и дублировать имена методов, которые вы пытаетесь смоделировать.
< EM> точечная / responses.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

1
Bert Blommers 20 Апр 2020 в 07:41

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

«Мне удалось зарегистрировать дополнительный сервис в Moto-framework для определения точки 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 () здесь для блага других людей, которые читают этот пост.

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

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

Mock_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_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_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_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

import boto3


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_app(name: str, push_tags: dict):
    client = boto3.client('pinpoint', region_name='eu-west-1')
    return client.create_app(
        CreateApplicationRequest={
            'Name': name,
            'tags': push_tags
        }
    )


def handler():
    application_id = get_parameter()
    app = create_app('my_app', {"my_tag": "tag"})
    return application_id, app

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']
        )

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

0
Dan-Dev 25 Апр 2020 в 17:41