【问题标题】:Does it make sense to write unit tests that are just testing if functions are called?编写仅测试是否调用函数的单元测试是否有意义?
【发布时间】:2019-07-16 17:14:02
【问题描述】:

我有以下代码:

def task_completed(task):
    _update_status(task, TaskStatus.COMPLETED)
    successful_task(task)


def task_pending(task):
    _update_status(task, TaskStatus.PENDING)
    successful_task(task)


def task_canceled(task):
    _update_status(task, TaskStatus.CANCELED)
    process_task(task)


def successful_task(task):
    process_task(task)
    send_notification(task)


def process_task(task):
    assign_user(task)
    notify_user(task)
    cleanup(task)


def _update_status(task, status):
    task.status = status
    task.save(update_fields=['status'])

我已经编写了以下测试:

def test_task_completed(mocker, task):
    mock_successful_task = mocker.patch('services.successful_task')
    task_completed(task)

    assert task.status == TaskStatus.COMPLETED
    mock_successful_task.called_once_with(task)


def test_task_pending(mocker, task):
    mock_successful_task = mocker.patch('services.successful_task')
    task_pending(task)

    assert task.status == TaskStatus.PENDING
    mock_successful_task.called_once_with(task)


def test_task_canceled(mocker, task):
    mock_process_task = mocker.patch('services.process_task')
    task_pending(task)

    assert task.status == TaskStatus.CANCELED
    mock_process_task.called_once_with(task)


def test_successful_task(mocker, task):
    mock_process_task = mocker.patch('services.process_task')
    mock_send_notification = mocker.patch('notifications.send_notification')

    mock_process_task.called_once_with(task)
    mock_send_notification.called_once_with(task)


def test_process_task(mocker, task):
    mock_assign_user = mocker.patch('users.assign_user')
    mock_notify_user = mocker.patch('notifications.notify_user')
    mock_cleanup = mocker.patch('utils.cleanup')

    mock_assign_user.called_once_with(task)
    mock_notify_user.called_once_with(task)
    mock_cleanup.called_once_with(task)

如您所见,test_successful_tasktest_process_task 等一些测试只是测试是否调用了特定函数。

但是为此编写测试是否有意义,还是我理解错误并且我的单元测试很糟糕?我不知道应该如何测试这些功能的另一种解决方案。

【问题讨论】:

    标签: python unit-testing testing


    【解决方案1】:

    根据我的经验,像这样的测试非常脆弱,因为它们依赖于实现细节。单元测试应该只关注被测试方法的结果。理想情况下,这意味着对返回值进行断言。如果有副作用,您可以断言它们。但是,您可能应该查看这些副作用并找到不需要它们的不同解决方案。

    【讨论】:

      【解决方案2】:

      我会说不,它们没有用。

      单元测试应该测试功能,这是一些输入,我称之为,这是我的结果,它是否符合我的预期?事情需要清楚和可验证。

      当您有一个验证方法已被调用的测试时,您真正拥有的是什么? 纯粹的不确定性。好的,已经调用了一个东西,但这有什么用呢?您没有验证结果,您调用的方法可以做一百万件事,而您不知道它做了什么。

      调用方法的代码是一个实现细节,您的单元测试不应该具备这种知识。

      我们为什么要编写单元测试? - 检查功能 - 帮助重构

      如果您需要在每次更改代码时都更改测试,那么您还没有真正完成单元测试的主要原因之一。

      如果您的代码更改并且不再调用该方法,那该怎么办? 你现在必须去改变测试?改成什么?如果您的下一步是删除测试,那么您为什么首先要删除?

      如果其他人必须在 6 个月后处理这个问题怎么办?没有文档可以检查并查看为什么有一个测试检查方法已被调用?

      底线,这样的测试具有零值,它所做的只是引入不确定性。

      【讨论】:

        【解决方案3】:

        白盒测试可用于检测某些回归或断言已执行特定操作。
        例如,您可以验证在这种特殊情况下您没有与数据库交互,或者您已正确调用通知服务。

        但是,缺点是您可能会在更改代码时更改测试,因为您的测试与实现密切相关。
        在重构时这可能会很痛苦,因为您还需要重构测试。您可能会忘记一个断言或一个步骤,并创建一个带有回归的误报测试。

        我只会在有意义的情况下使用它,并且如果您需要它来详细说明正在发生的事情。

        您可以在网络上搜索 TDD:伦敦 vs 底特律。
        你会发现有趣的东西。

        【讨论】:

        • 我也找到了“伦敦 vs 芝加哥”。在这种情况下,芝加哥和底特律是一样的吗?
        • 是的,我想是的
        • @ArnaudClaudel 如果我通过检查用户是否被分配给给定任务并且用户是否收到消息来测试 process_task 功能会更好。因为这是函数的目的,这基本上是函数被调用后我想要的状态。但是我也对 process_task 函数中的函数进行了单元测试,所以代码会有点重复。
        • 你的意思是,只测试 process_task 并删除其他测试?
        • 我认为,如果您的服务被很好地隔离和测试,是的,当您知道该服务已被调用时,您可以验证您的测试。但有时您想要进行完整的端到端测试,然后您必须再次断言所有内容。在这种情况下,您应该重构您的测试并创建一个执行所有断言的函数,以便您可以在集成和端到端测试中重用它。
        【解决方案4】:

        这并不是单元测试的目的,尽管它确实有用。单元测试旨在通过​​测试功能和结果来提高代码的质量——编写单元测试来测试每个调用方法的功能会更有益。

        话虽如此,如果您有一个函数调用 4 个其他函数,并且您想检查它们是否真的在您的主代码块中执行,那么这是有道理的。但是你绝对应该为你的子方法编写单元测试。

        【讨论】:

          【解决方案5】:

          是的,这是有道理的。不过,我会看unittest.mock.Mock.assert_called_with

          【讨论】:

            【解决方案6】:

            根据我的经验,是的。

            当你设计一个测试时,你知道你必须处理 4 个元素

            • 前置条件(上下文)
            • 输入
            • 输出
            • 后置条件(副作用)

            我们都同意,如果被测函数的行为仅取决于输入和输出,则测试和编码会更容易,但在某些情况下,这不会发生,尤其是当您的逻辑处理 I/O 和/或者它的目标是发出应用程序状态的突变。这意味着您的测试必须了解后置条件。但是什么可以确保我们满足后置条件呢?

            选择这个方法

            public class UserService
            {
                public void addUser(User toAdd)
            }
            

            此方法在数据库中添加一个用户;以更优雅的方式,我们可以说将用户添加到集合中,该集合由存储库语义抽象。所以该方法的副作用是调用了一个 userRepository.save(User user)。您可以模拟此方法并期望已使用给定参数调用过一次,或者使测试失败

            显然,只有在方法被模拟后才能实现这一点,因此测试不会受到未测试单元的行为的影响。

            我承认缺点是使测试变脆,但同时

            1. 在 tdd 中,它会进行不调用模拟函数的失败测试,​​因此测试状态为“嘿,addUser 依赖于 UserRepository.save()!”如果你喜欢这种风格

            2. 如果依赖函数接口发生变化,测试就会被打破,但是我们不想经常改变接口,对吗?

            3. 在向方法添加依赖项之前,您会三思而后行,这是编写更简洁代码的提示

            【讨论】:

              猜你喜欢
              • 2021-07-20
              • 1970-01-01
              • 2016-08-07
              • 1970-01-01
              • 2014-05-11
              • 1970-01-01
              • 1970-01-01
              • 2013-08-22
              • 1970-01-01
              相关资源
              最近更新 更多