如何使此功能更易于测试?

时间:2019-01-09 11:48:56

标签: python django unit-testing testing

我具有使用textattachments构建并返回Slack消息的功能。如何重构此功能以使其更易于测试?我应该将其拆分为多个功能吗?

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 :(得分:1)

您几乎应该确定将其拆分为多个功能。您将拥有更少的更简单的测试用例。

让我们看看如何做到这一点。因为您希望添加更多测试,所以我会坚持安全重构,您可以在像PyCharm这样的IDE中进行此操作。

注意:我正在SO应答编辑器中进行此重构,因此未经测试。可能是拼写错误或缺少参数。

首先让我惊讶的是,您对pagesasync_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
    }

因此,现在您可以为page_and_async_task编写6个测试,然后在测试build_list_message时模拟该函数。模拟功能只需要返回有效的页面和async_tasks。

接下来,我将考虑单一责任原则。您的函数负责使用键textattachments构建字典。它可以委派给其他东西。

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
    }

现在,您可以测试顶级功能,模拟它的助手。然后,对于您制作的每个模拟,您都需要对帮助程序进行测试,以显示其行为与模拟完全相同,一直到最后。然后编写一个运行真实事物并显示所有功能协同工作的测试。

在此之后的步骤中,您可能希望应用策略模式来简化注入辅助功能的过程。