【问题标题】:Unit testing composite functions单元测试复合函数
【发布时间】:2017-08-12 20:35:57
【问题描述】:

假设你有 3 个函数,函数 A、函数 B 和函数 C

functionC 依赖于 functionA 和 functionB

functionA(a) {
    return a
}
functionB(b) {
     return b
}
functionC(a, b){
     return functionA(a) + functionB(b);
}

现在这显然是一个超级简化的例子.. 但是测试functionC的正确方法是什么?如果我已经在测试 functionA 和 functionB 并且它们通过了,那么测试 functionC 最终不仅仅是一个单元测试,因为它将依赖于 functionA 和 functionB 返回......

【问题讨论】:

  • 是否有任何函数调用其他函数?即使它只是标准库函数?这本身并不是一个标准。

标签: unit-testing testing


【解决方案1】:

对于前两个函数,您将专注于他们的公共合同 - 您将根据需要编写尽可能多的测试,以确保不同案例的所有结果都符合预期。

但是对于第三个函数,理解该函数应该调用其他两个函数可能就足够了。所以你可能需要更少的测试。您不需要再次测试测试 A 和 B 所需的所有案例。您只需验证 C 负责的预期“管道”。

【讨论】:

    【解决方案2】:

    在我看来,您的测试应该不知道 functionC 正在使用 functionA 和 functionB。通常您会创建自动测试,以支持更改(代码维护)。如果你改变 C 的实现怎么办? functionC 的所有测试也变得无效,这是不必要且危险的,因为这意味着重构者也必须了解所有测试。即使他/她确信,他/她不会更改合同。如果您的测试覆盖率很高,他/她为什么要这样做?所以要对functionC的公共合约进行完整的测试!

    还有一个进一步的危险,如果测试对 sut(functionC) 的内部工作了解太多,他们往往会重新实现里面的代码。因此,执行实现的相同(可能是错误的)代码会检查实现是否正确。 举个例子:你将如何实现functionC的(白盒)测试。模拟 functionA 和 functionB 并查看是否产生了模拟结果的总和。这对测试覆盖率(kpi??)有好处,但也可能会产生误导。

    但是,两次测试 functionA 和 functionB 的功能所付出的高额外工作又如何呢?如果是这样,那么可能很容易重用测试代码,如果不能重用,我认为这更证实了我之前的陈述。

    【讨论】:

    • 如果 functionA 调用另一个有副作用的函数(如数据库更新)怎么办?在测试functionC时,你会存根a-tree的最后一个有副作用的函数吗?如果是这样,那将需要在所有使用 functionA 的测试函数中重复?或者你会存根 functionA 并返回一个模拟以避免副作用?
    • @henit 我不会避免副作用,但也要检查一下。每个测试都应该从一个空数据库开始,或者至少从同一个数据库状态开始。否则会出现闪烁测试。这必须是测试基础设施的一部分。
    • 如果副作用超出了测试架构怎么办?就像对外部 api 的 http 调用
    • @henit 这些要通过测试来模拟和控制。有很好的策略可以做到这一点。我说的是模块测试。我的目标是测试可部署的黑盒。外部服务将被模拟。尤其是内存版本中的 Dbms 和 Jms,可以轻松清除。
    【解决方案3】:

    GhostCat 的答案很简单、很好,并且专注于本质。
    我将详细介绍其他一些需要考虑的问题,尤其是重构问题。


    单元测试侧重于 API

    类 API(公共函数)必须经过单元测试。
    如果这 3 个功能是公开的,则每个功能都必须进行测试。

    此外,单元测试不关注实现,而是关注预期行为。
    今天,复合函数添加单个函数结果,明天它可以减去它们或其他任何结果。
    测试C() 复合函数并不意味着再次测试A()B() 的所有场景,它意味着测试C() 的预期行为。

    在某些情况下,对与单个函数集成的复合函数进行单元测试不会产生很多关于单个函数的重复。
    在其他情况下,确实如此。我将在下一点介绍它。


    测试C() 复合函数可能会导致测试中出现重复问题的示例。

    假设A() 函数接受两个整数:

    function A(int a, int b){ ...}
    

    它对输入参数有以下限制:

    • 它们必须 >=0
    • 他们已经低于 100
    • 他们的总和小于100

    如果其中一项没有得到遵守,则会引发异常。 在A() 单元测试中,我们将测试这些场景中的每一个。每一个都可能在一个不同的测试用例中:

    @Test
    void A_throws_exception_when_one_of_params_is_not_superior_or_equal_to_0(){ 
         ...
    }
    
    @Test(expected = InvalidParamException.class);
    void A_throws_exception_when_one_of_params_is_not_inferior_to_100(){ 
         ...
    }
    
    @Test(expected = InvalidParamException.class);
    void A_throws_exception_when_params_sum_is_not_inferior_to_100(){ 
         ...
    }
    

    除了错误情况,我们还可以根据传递的参数为A() 函数提供多个标称场景。

    假设B() 函数也有多个标称和错误场景。

    那么聚合它们的C() 的单元测试呢?
    您当然不应该重新测试这些案例中的每一个。它有很多重复,而且通过交叉两个函数的情况会有更多的组合。
    下一点介绍如何防止重复。


    可能的重构以改进设计并减少复合功能单元测试中的重复

    在编写复合函数时,首先要考虑的是复合函数是否不应位于特定组件中。

    composite component -> unitary component(s)
    

    将它们解耦可以改善整体设计并赋予组件更具体的职责。
    此外,它还提供了一种自然的方法来减少复合组件的单元测试中的重复。
    实际上,如果需要,您可以存根/模拟单一组件行为,而无需为它们创建详细的固定装置。
    复合组件单元测试可以专注于复合组件的行为。

    因此,在我们之前的示例中,我们可以对C() 函数进行单元测试,而不是测试A()B() 的所有情况,而是可以存根或模拟A()B() 以便它们的行为正如C() 场景所预期的那样。

    例如对于带有与A()B() 相关的错误案例的C() 测试场景,我们不需要重复每个A()B() 场景案例:

    @Test(expected = InvalidParamException.class);
    void C_throws_exception_when_a_param_is_invalid(){ 
         when(A(any,any)).thenThrow(new InvalidParamException());
         C();
    }
    
    @Test(expected = InvalidParamException.class);
    void C_throws_exception_when_b_param_is_invalid(){ 
         when(B(any,any)).thenThrow(new InvalidParamException());
         C();
    }
    

    【讨论】:

    • 有趣的是,我今天在这里的回答赢得了我的第一个“救生衣”银质徽章。周围有一些我从未听说过的东西。在哪里,我故意从不检查这些超级特殊的徽章,这些徽章只有在满月照在半潮时才会收到,而你用一只手绑在背后或其他什么东西写答案......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-09-23
    • 2011-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多