Unit testing 组合函数的单元测试

Unit testing 组合函数的单元测试,unit-testing,testing,Unit Testing,Testing,假设你有3个函数,函数A,函数B和函数C 函数C依赖于函数A和函数B functionA(a) { return a } functionB(b) { return b } functionC(a, b){ return functionA(a) + functionB(b); } 这显然是一个超级简化的例子。。但是测试functionC的正确方法是什么?如果我已经在测试functionA和functionB,并且它们通过了测试,那么functionC的测试就不仅仅

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

函数C依赖于函数A和函数B

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

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

对于前两个函数,您将关注它们的公共契约-您将根据需要编写尽可能多的测试,以确保不同函数的所有结果案件如预期的那样


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

我认为您的测试应该不知道functionC正在使用functionA和functionB。通常创建自动测试,以支持更改(代码维护)。如果你改变了C语言的实现呢?functionC的所有测试也都会失效,这是不必要的,也是危险的,因为这意味着重构者也必须理解所有的测试。即使他/她确信,他/她不会更改合同。如果你有很好的测试覆盖率,他/她为什么要这么做?因此,functionC的公共合同需要全面测试

还有一个进一步的危险,如果测试对sut(functionC)的内部工作原理了解得太多,它们会倾向于重新实现内部代码。因此,执行实现的代码(可能是错误的)会检查实现是否正确。 举个例子:您将如何实现functionC的(白盒)测试。模拟functionA和functionB,并查看是否生成模拟结果的总和。这对于测试覆盖率(kpi???)来说很好,但也可能会产生误导


但是,对functionA和functionB的功能进行两次测试的高额外工作量如何呢。如果是这样的话,那么可能很容易重用测试代码,如果重用不可能,我认为这更能证实我前面的陈述。

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();
}

是否有任何函数不调用其他函数?即使只是标准的库函数?这本身并不是一个标准。如果function调用另一个有副作用的函数(如数据库更新),该怎么办?在测试functionC时,您会存根a的最后一个函数吗-