如何确保被测函数是唯一被调用的函数?

时间:2015-05-06 21:38:24

标签: python unit-testing testing

我使用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)]

我对extract_primes的测试结果感到困惑。

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#依赖注入指令,它们告诉你将正确的所需对象作为参数传递,并为你的方法创建该对象的虚拟对象# 39;重新测试。或者我讨论是否要测试私有方法。那很好,我理解那些事情。但我不能为我的生活弄清楚如何处理依赖于其他函数调用的函数。我应该简单地注入这些功能吗?

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语句。

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

我有点失落。

3 个答案:

答案 0 :(得分:2)

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

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

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

此外,通过这种方式,您可以执行以下操作:尝试其他主要测试函数,使用生成器表达式替换列表推导,或者在引擎盖下替换其他重构等,无需需要更改测试代码。

应该为外部依赖项保留模拟;修补任何函数所做的每一次调用都是荒谬冗长的,将测试代码与实现联系得太紧,而且并没有真正为你提供任何帮助。

答案 1 :(得分:1)

一般来说,当您达到最佳实践不适合您的点时,这是一个很好的经验法则,那么问题发生的时间要早​​于该点。在你的情况下,你不知道该怎么做,因为你有两个公共方法互相调用。这通常表明它们不属于同一类。例如,基于谓词过滤列表的函数实际上与素数没有任何关系。这不应该是这个类提供的功能。如果您经常这样做,那么您可以创建一个具有过滤方法的list utils类。我认为测试这两种方法is_primefilter_list应该非常简单。

通常,类应设计为向用户提供一组功能。一个类不应该是它自己的用户。这造成了许多问题,其中最重要的是你遇到的问题。当然这个规则有例外,最好的设计应该根据具体情况来判断,但我发现这是一个很好的经验法则。

答案 2 :(得分:1)

不幸的是,没有简单的答案,缺少&#34;编写结构良好的代码&#34;,当然,这很难。我将尝试通过逐个解决你的问题来帮助阐明这个问题。

  

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

这是大致正确的,但主要目标是测试该代码的接口;也就是说,给定某些输入,你会得到预期的输出吗?你不想关心如何那段代码做你想做的事情(如果你这样做,你开始编写change detector测试)。

  

我不想测试[我的功能]调用的任何函数。

不完全;你不想关心被测试代码调用的函数。就您的测试而言,如果它是输入到输出的硬编码映射,或者如果它熄灭并进行Google搜索*并解析第一个结果,则不必关心,只要行为是你所期待的。

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

  

我只是隐式测试is_prime是否正常工作。

不是真的。 extract_primes()没有任何要求它使用is_prime()的内容,实际上您可能会在以后发现更好的此行为算法,并希望将其交换出来。您希望单独测试这两个函数,否则您的测试只能假设extract_primes()调用is_prime()

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

  

is_prime出现错误时,它还会导致extract_primes的测试失败,这会超出单元测试的范围,您希望快速发现单点故障。

非常正确,这通常是您想要避免的事情。但是所有代码都是基于其他代码构建的,并且总是只有一个测试失败是不可能的。如果代码中的某些内容破坏了,那么依赖它的所有东西都会破坏。一种解决方案是进行多层测试;单位和集成测试至少,但你也可以在其中有层,例如首先测试您的实用程序代码,并且只有在这些测试都通过后才测试相关代码。

  

[注入功能]充其量且极不灵活。

当然。不要这样做(通常)。事实上,对代码的一个非常好的嗅探测试是&#34;为了编写好的测试,我是否必须使用我的代码做一些奇怪,繁琐的事情?&#34;如果是这样,你的设计可能值得重访。

当然,这并不总是可行的。您已经看到了模拟,存根,猴子修补和依赖注入,所有这些都是处理难以测试的代码的工具,但这些都是当您无法可靠测试时的工具界面

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

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