Как сделать эту функцию проще для тестирования? - PullRequest
0 голосов
/ 09 января 2019

У меня есть функция, которая создает и возвращает сообщение Slack с text и attachments. Как я могу изменить эту функцию для упрощения тестирования? Должен ли я разделить его на несколько функций?

def build_list_message(team_id, user_id, msg_state=None, chl_state=None):
    if not msg_state:
        msg_state = {}

    if not chl_state:
        chl_state = {}

    resource_type = msg_state.get('resource_type', 'all')
    availability = msg_state.get('resource_availability', 'all')

    pages = Page.objects.none()
    async_tasks = AsyncTask.objects.none()

    if resource_type in ['web_pages', 'all']:
        pages = Page.objects.filter(
            user__team__team_id=team_id).order_by('title')

    if resource_type in ['async_tasks', 'all']:
        async_tasks = AsyncTask.objects.filter(
            user__team__team_id=team_id).order_by('title')

    if availability == 'available':
        pages = pages.filter(available=True)
        async_tasks = async_tasks.filter(available=True)

    elif availability == 'unavailable':
        pages = pages.filter(available=False)
        async_tasks = async_tasks.filter(available=False)

    channel_id = chl_state.get('channel_id')
    if channel_id:
        pages = pages.filter(alert_channel=channel_id)
        async_tasks = async_tasks.filter(alert_channel=channel_id)

    user = SlackUser.retrieve(team_id, user_id)

    attachments = [
        _build_filters(resource_type, availability),
        *[_build_page_item(p, user) for p in pages],
        *[_build_async_task_item(at, user) for at in async_tasks]
    ]

    return {
        'text': "Here's the list of all monitoring resources",
        'attachments': attachments
    }

Вот приватные функции:

def _build_filters(resource_type, availability):
    resource_types = [
        {"text": "All types", "value": "all"},
        {"text": ":link: Webpages", "value": "web_pages"}
    ]

    availability_choices = [
        {"text": "Available / Unavailable", "value": "all"},
        {"text": ":white_circle: Available", "value": "available"},
        {"text": ":red_circle: Unavaliable", "value": "unavailable"}
    ]

    selected_resource_types = list(filter(
        lambda t: t['value'] == resource_type, resource_types))

    selected_availability_choices = list(filter(
        lambda a: a['value'] == availability, availability_choices))

    return {
        "fallback": "Resource filters",
        "color": "#d2dde1",
        "mrkdwn_in": ["text"],
        "callback_id": "resource_filters",
        "actions": [
            {
                "name": "resource_type",
                "text": "Type",
                "type": "select",
                "options": resource_types,
                "selected_options": selected_resource_types
            },
            {
                "name": "resource_availability",
                "text": "Available",
                "type": "select",
                "options": availability_choices,
                "selected_options": selected_availability_choices
            }
        ]
    }


def _build_page_item(page, user):
    return {
        "fallback": "Page",
        "color": page.status_color,
        "mrkdwn_in": ["fields"],
        "callback_id": 'page_change',
        "fields": [
            {
                "title": page.title,
                "value": f"_Page_ ({page.status})"
            },
            {
                "title": "URL",
                "value": page.url
            }
        ],
        "footer": _build_resource_footer(page),
        "actions": _build_resource_item_actions(page, user)
    }


def _build_async_task_item(async_task, user):
    return {
        "fallback": "Async task",
        "color": async_task.status_color,
        "mrkdwn_in": ["fields"],
        "callback_id": 'async_task_change',
        "fields": [
            {
                "title": async_task.title,
                "value": f"_Async task_ ({async_task.status})"
            },
            {
                "title": "URL",
                "value": async_task.url
            }
        ],
        "footer": _build_resource_footer(async_task),
        "actions": _build_resource_item_actions(async_task, user)
    }

1 Ответ

0 голосов
/ 10 января 2019

Вы почти наверняка должны разделить это на несколько функций. У вас будет меньше тестовых случаев, которые будут проще.

Давайте посмотрим, как это сделать. Поскольку вы хотите добавить больше тестов, я остановлюсь на безопасном рефакторинге, который вы можете выполнять в среде IDE, такой как PyCharm.

ПРИМЕЧАНИЕ: я делаю этот рефакторинг в редакторе ответов SO, поэтому он не проверен. Возможны опечатки или пропущенные параметры.

Первое, что бросается в глаза, это то, что у вас есть несколько заданий на pages и async_tasks

def build_list_message(team_id, user_id, msg_state=None, chl_state=None):
    ...

    pages = Page.objects.none()
    async_tasks = AsyncTask.objects.none()

    if resource_type in ['web_pages', 'all']:
        pages = Page.objects.filter(
            user__team__team_id=team_id).order_by('title')

    if resource_type in ['async_tasks', 'all']:
        async_tasks = AsyncTask.objects.filter(
            user__team__team_id=team_id).order_by('title')

    if availability == 'available':
        pages = pages.filter(available=True)
        async_tasks = async_tasks.filter(available=True)

    elif availability == 'unavailable':
        pages = pages.filter(available=False)
        async_tasks = async_tasks.filter(available=False)

    channel_id = chl_state.get('channel_id')
    if channel_id:
        pages = pages.filter(alert_channel=channel_id)
        async_tasks = async_tasks.filter(alert_channel=channel_id)

    ...

Похоже, что это связные значения, основанные на коде, поэтому я собираюсь извлечь одну функцию, которая выбирает их.

def page_and_async_task(resource_type, availability, team_id, chl_state):
    pages = Page.objects.none()
    async_tasks = AsyncTask.objects.none()

    if resource_type in ['web_pages', 'all']:
        pages = Page.objects.filter(
            user__team__team_id=team_id).order_by('title')

    if resource_type in ['async_tasks', 'all']:
        async_tasks = AsyncTask.objects.filter(
            user__team__team_id=team_id).order_by('title')

    if availability == 'available':
        pages = pages.filter(available=True)
        async_tasks = async_tasks.filter(available=True)

    elif availability == 'unavailable':
        pages = pages.filter(available=False)
        async_tasks = async_tasks.filter(available=False)

    channel_id = chl_state.get('channel_id')
    if channel_id:
        pages = pages.filter(alert_channel=channel_id)
        async_tasks = async_tasks.filter(alert_channel=channel_id)

    return pages, async_tasks

def build_list_message(team_id, user_id, msg_state=None, chl_state=None):
    if not msg_state:
        msg_state = {}

    if not chl_state:
        chl_state = {}

    resource_type = msg_state.get('resource_type', 'all')
    availability = msg_state.get('resource_availability', 'all')

    pages, async_tasks = page_and_async_task(resource_type, availability, team_id, chl_state)

    user = SlackUser.retrieve(team_id, user_id)

    attachments = [
        _build_filters(resource_type, availability),
        *[_build_page_item(p, user) for p in pages],
        *[_build_async_task_item(at, user) for at in async_tasks]
    ]

    return {
        'text': "Here's the list of all monitoring resources",
        'attachments': attachments
    }

Итак, теперь вы можете написать 6 тестов для page_and_async_task, а затем смоделировать эту функцию при тестировании build_list_message. Имитирующая функция просто должна возвращать действительные страницы и async_tasks.

Далее я собираюсь рассмотреть принцип единой ответственности. Ваша функция отвечает за построение диктата с помощью клавиш text и attachments. Он может делегировать что-то еще.

def build_list_message(team_id, user_id, msg_state=None, chl_state=None):
    if not msg_state:
        msg_state = {}

    if not chl_state:
        chl_state = {}

    resource_type = msg_state.get('resource_type', 'all')
    availability = msg_state.get('resource_availability', 'all')

    pages, async_tasks = page_and_async_task(resource_type, availability, chl_state)

    user = SlackUser.retrieve(team_id, user_id)

    attachments = make_attachments(resource_type, availability, pages, async_tasks, user)

    return {
        'text': "Here's the list of all monitoring resources",
        'attachments': attachments
    }

def make_attachments(resource_type, availability, pages, async_tasks, user):
    return [
        _build_filters(resource_type, availability),
        *[_build_page_item(p, user) for p in pages],
        *[_build_async_task_item(at, user) for at in async_tasks]
    ]

Точка входа по-прежнему делает слишком много - она ​​разбивает входные данные, вызывает SlackUser.retrieve, который, по-видимому, обращается к базе данных, и создает сообщение.

def build_list_message(team_id, user_id, msg_state=None, chl_state=None):
    if not msg_state:
        msg_state = {}

    if not chl_state:
        chl_state = {}

    resource_type = msg_state.get('resource_type', 'all')
    availability = msg_state.get('resource_availability', 'all')
    user = SlackUser.retrieve(team_id, user_id)

    return _build_list_message(team_id, user_id, resource_type, availability, chl_state, user)


def _build_list_message(resource_type, availibility, chl_state, user):
    pages, async_tasks = page_and_async_task(resource_type, availability, chl_state)

    attachments = make_attachments(resource_type, availability, pages, async_tasks, user)

    return {
        'text': "Here's the list of all monitoring resources",
        'attachments': attachments
    }

Теперь вы можете протестировать функцию верхнего уровня, высмеивая ее помощников. Затем за каждый сделанный вами макет вы должны пройти тест для помощника, который показывает, что он ведет себя так же, как и макет, вплоть до самого конца. Затем напишите один тест, который запускает реальную вещь и показывает, что все работает вместе.

В следующих шагах вы можете применить шаблон стратегии, чтобы упростить внедрение вспомогательных функций.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...