【问题标题】:How do I make sure that the function under test is the only function being called?如何确保被测函数是唯一被调用的函数?
【发布时间】:2015-05-06 21:38:24
【问题描述】:

如果相关的话,我正在使用 Python。

据我了解,单元测试是对与单个用例相关的最小代码部分的测试。通常这是一个单一的功能或方法。

所以当我测试我的函数时,我不想测试它正在调用的任何函数。在编写测试时,这很容易做到:只需关注函数正在执行的逻辑即可。

但是当我遇到以下情况时:

def is_prime(number):
    if number <= 1:
        return False

    for element in range(2, number):
        if number % element == 0:
            return False

    return True

def extract_primes(lst):
    return [item for item in list if is_prime(item)]

我对@9​​87654323@ 的测试看起来如何感到困惑。

def test_extract_primes():
    lst = [0, 1, 2]
    result = extract_primes_from_list(lst)
    assert result == [2]
    # and some more similar tests for empty lists, lists with only primes
    # and lists with no primes

但现在我只是在隐式测试is_prime 是否正常工作。而当is_prime 出现错误时,也会导致extract_primes 的测试失败,这超过了单元测试的要点,您希望在其中快速发现单点故障。

所以在我看来,我一开始就不应该打电话给is_prime。所以我最终得到了这样的结果:

@unittest.mock.patch("path.to.module.is_prime")
def test_extract_primes(mock_is_prime):
    mock_is_prime.return_value = True
    lst = list(range(10))
    result = extract_primes(lst)
    assert result == lst
    for i in lst:
        assert mock_is_prime.called_with(i)  # not sure if correct method name

但这充其量是令人厌烦且不灵活的。我不能让mock_is_prime 在同一个测试中返回不同的值,除非我创建一个类似的构造:

bool_list = [True, False, True]
mock_is_prime.side_effect = lambda x: next(bool_list)

但这会让更加冗长。随着被测函数中函数调用次数的增加,用于修补这些函数的愚蠢样板代码数量也在增加。

当我尝试在互联网上寻找答案时,我大多会得到 Java/C# 依赖注入指令,它们会告诉你传递正确的所需对象作为参数,然后为你的方法创建一个该对象的虚拟对象'重新测试。或者我会争论是否要测试私有方法。没关系,我理解那些事情。但是我一辈子都无法弄清楚如何处理依赖于其他函数调用的函数。我应该简单地注入这些函数吗?

def extract_primes(lst, is_prime_function=is_prime):
    return [item for item in list if is_prime_function(item)]

然后将一个虚拟的is_prime 函数传递给正在测试的extract_primes?这看起来很愚蠢,并且在函数签名中乱扔了奇怪的参数。这实际上与在所需的工作量上修补功能没有什么不同。它只是从测试中删除了一个 @patch 语句。

所以我不应该首先修补功能吗?如果不是,那么一个函数在什么时候才值得修补?只有当它操纵系统时?或者当它来自一个完全独立的模块时?

我有点失落。

【问题讨论】:

  • 这可能更适合programmers.SE。
  • 另外:这并不能回答你的问题,但是 -- extract_primes = partial(filter, is_prime)
  • @tzaman 我很清楚。我只需要示例代码。实际代码很难在问题中快速解释。每个人都知道素数是什么。所以我会用一个关于素数的问题。

标签: python unit-testing testing


【解决方案1】:

一般来说:这完全取决于您的设计,但您最初的尝试似乎是合理的。

假设is_prime 和extract_primes 都是你的类/模块的公共接口的一部分,它们都应该被测试。此时,您想将extract_primes 测试为一个黑盒,即只是确保它履行其合同:给它一个列表并返回素数。它在内部使用is_prime 的事实是您的单元测试代码不应该关注的实现细节。

单元测试的重点不一定是只触发最早的失败(当然,如果你能做到这一点,那就太好了);如果你在上游破坏了足够高的东西,很多依赖的东西很可能会失败。如果is_prime 被破坏,那么看到extract_primes 的测试失败也是完全有效的,但只需查看测试失败列表就足以立即确定根本原因。

此外,通过这种方式,您可以执行以下操作:尝试其他主要测试功能,用生成器表达式替换列表推导,或在后台进行其他重构等 而无需更改您的测试代码。

Mocking 应该保留给 external 依赖;修补任何函数进行的每个调用都非常冗长,将测试代码与实现联系得太紧,并没有真正得到任何东西。

【讨论】:

    【解决方案2】:

    一般来说,一个很好的经验法则是,当您达到最佳做法对您不起作用的地步时,问题发生的时间要早​​得多。在您的情况下,您不知道该怎么做,因为您有两个相互调用的公共方法。这通常表明它们不属于同一类。例如,基于谓词过滤列表的函数实际上与素数没有任何关系。这不应该是此类提供的功能。如果这是您经常做的事情,那么您可以创建一个具有过滤器方法的列表实用程序类。我认为测试is_prime和filter_list这两种方法应该很容易。

    一般来说,类的设计应该为用户提供一组功能。一个类不应该是它自己的用户。这会产生许多问题,其中最重要的是您遇到的问题。当然,这条规则也有例外,最好的设计应该根据具体情况来判断,但我认为这是一个很好的经验法则。

    【讨论】:

    • 当然一个类可以是它自己的用户。 Java 一直使用 getter 和 setter 来做到这一点。并考虑一个类ChessBoard,它具有do_move 和is_move_legal 方法,这两个方法都是公共的。 do_move 方法调用is_move_legal,因为棋盘不能执行非法移动。但我什至没有在上面的例子中使用一个类;只有程序代码。
    • 再一次,getter 和 setter 几乎总是糟糕设计的指标。公开您的会员是一种不那么冒犯的方式。
    • @CarmenBiancaBakker 虽然我同意“getter 和 setter 几乎总是不良设计的指标”听起来有点刺耳,但没必要说人们疯了。从广义上讲,Benjy 是对的,例如可能不需要向调用者公开像is_move_legal 这样的方法。您的 try/catch 示例有些做作;来电者不需要“撤消”董事会,这是董事会的工作。在任何情况下,数据结构设计决策都与这个问题相切。当您遇到测试困难时,您可能需要重新检查之前的决定,这一点是正确的。
    • @CarmenBiancaBakker 如果您想就此提出问题,我很乐意与您讨论ChessBoard 设计决策。然而,就这个问题而言,只要说您可以成功地实现ChessBoard 类而不将is_move_legal 暴露给调用者就足够了。 Benjy 和我的建议是这样的设计可能更容易测试。看到您来这里寻求有关如何测试事物的帮助,也许更认真地对待这个建议。而这里唯一一个“疯狂”的人就是你。请停下来。
    • 当您需要测试 private 方法时,该方法有 95% 的可能性应该是私人协作者的公共方法。也许is_move_legal 是ChessRules() 类的公共方法,用作ChessBoard 的合作者。单一职责原则规则......当一个类做太多工作时,它会拆分它,也许你的设计会得到改进并变得更可测试。
    【解决方案3】:

    不幸的是,没有简单的答案,除了“编写结构良好的代码”,当然,这很难。我将通过逐个回答您的问题来帮助阐明这个主题。

    据我了解,单元测试是对与单个用例相关的最小代码部分的测试。通常这是一个单一的功能或方法。

    这大体上是正确的,但主要目标是测试该代码的接口;也就是说,给定一定的输入,你是否得到了预期的输出。你不想关心代码块如何做你想做的事情(如果你这样做了,你就开始编写change detector测试)。

    我不想测试[我的函数正在]调用的任何函数。

    不完全;你不想关心被测试的代码调用了什么函数。就您的测试而言,您不在乎它是否是输入到输出的硬编码映射,或者它是否出去并执行 Google 搜索*并解析第一个结果,只要行为是什么你期待。

    请注意,您的 is_prime 函数也取决于其他“函数”的行为 - &lt;=、range()、% 和 == 对于您的 is_prime 函数都必须以某种方式运行正常工作。那不是坏事!您可以确信 is_prime 所依赖的功能正常工作,因为 Python 已经对其进行了测试。是否同时测试它们并不重要。

    我只是在暗中测试is_prime 是否正常工作。

    不是真的。 extract_primes() 并没有什么要求它使用 is_prime(),事实上你很可能会在以后发现一个更好的算法来解决这个问题并想把它换掉。您想分别测试这两个函数,否则您的测试只能在extract_primes() 调用is_prime() 的假设下工作。

    如果在您的实际用例中有某些原因extract_primes() 必须依赖is_prime(),那么您可能会过度耦合您的代码。正如其他人所建议的那样,考虑将您的类似谓词的行为与类似过滤器的行为分开,以便过滤适用于任何谓词。

    当is_prime 出现错误时,也会导致extract_primes 的测试失败,这超过了单元测试的要点,您希望快速发现单点故障。

    非常正确,这通常是您要尽量避免的事情。但是所有代码都是建立在其他代码之上的,不可能总是只有一个测试失败。如果代码深处的某些东西损坏了,那么依赖它的所有东西也会损坏。一种解决方案是进行多层测试;至少要进行单元和集成测试,但您也可以在其中包含层,例如首先测试您的实用程序代码,并且仅在这些测试全部通过后才测试相关代码。

    [注入函数]是令人厌烦且不灵活的。

    当然。不要那样做(通常)。事实上,一个很好的代码嗅探测试是“为了编写好的测试,我是否必须对我的代码做一些奇怪、繁琐的事情?”如果是这样,您的设计可能值得重新审视。

    当然,这并不总是可能的。您已经看到了 mocking、stubbing、monkey patching 和依赖注入,所有这些都是处理难以测试的代码的工具,但这些都是当您无法可靠地测试接口时的工具 em>。

    换句话说,首先尝试编写干净、松散耦合的代码。然后,尝试单独测试各个组件的接口。当你不能可靠地做到这一点时,开始研究模拟和依赖注入等等。

    *从性能或测试稳定性的角度来看,您当然可能会关心这一点,但这是一个单独的问题。就个人测试而言,它并不关心。

    【讨论】:

    • 我有点理解,但我还是有点困惑。这尤其令人困惑,因为很多单元测试指令都非常受 OOP 约束,而 Python 代码很容易混合各种样式。 This blog post 比较清楚地描述了这个问题。如果我有一个函数调用一个函数,该函数又调用了两个函数,那么测试第一个函数就变得异常困难。而且因为所有的函数都是纯函数,所以除了实时补丁之外没有依赖注入。
    • @CarmenBiancaBakker 我想要表达的核心观点是“不要担心你的函数是否会调用其他函数,除非你必须这样做。”如果您需要公开is_prime 和extract_primes 函数,那么您应该对它们进行测试。如果您发现自己编写了大量冗余测试、变更检测器或易碎测试,那么强烈建议您重新审视您的设计,这样您就可以同时提供这两种行为,而不会相互依赖。对于您提供的示例,定义通用过滤器方法并进行更清洁的测试。
    猜你喜欢
    • 1970-01-01
    • 2011-05-09
    • 2019-05-03
    • 1970-01-01
    • 2016-11-16
    • 1970-01-01
    • 2014-08-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多