討論交流
我的两分钱 2023-01-10 151 0 0 0 0
测试,最近看到了《UnitTestingPrinciples,Practices,andPatterns》一书的作者VladimirKhorikov关于是否应该对私有方法进行单元测试(UT)的讨论,我觉得说…

最近看到了《Unit Testing Principles, Practices, and Patterns》一书的作者Vladimir Khorikov关于是否应该对私有方法进行单元测试(UT)的讨论,我觉得说得比较清晰,所以也在这里做一个介绍。

这里先说结论--不要对私有方法进行UT

因为一个方法如果被声明为了私有,那么就是说该方法是一个实现细节,不应该被外部所关注。如果我们对私有方法进行测试,那么我们的测试代码就和生产代码产生了耦合,这会导致我们的测试变得脆弱。

在这种耦合下,当生产代码改变其内部实现细节或者进行重构时,就产生了一个两难问题:如果我们不跟着修改测试代码,那么就有可能产生很多误报错;如果我们跟着修改测试代码,测试代码的维护工作量又大大增加了。

但私有方法往往影响着类对外公开的行为,如果不对私有方法进行测试,我们如何确保我们的类能正确工作呢?答案是对类的公开行为进行充分测试,如果我们的测试覆盖率足够,我们就能间接覆盖到私有方法的所有执行路径,如果我们覆盖了足够的use case,我们就有信心我们的类能正确工作。

有经验的同学会问,如果某个私有方法特别复杂,即使对类的公开行为做了足够测试,也不足以让我们对该私有方法完全放心该怎么办?Vlad给出了这样一个例子:

public class Order
{
private Customer _customer;
private List<Product> _products;

public string GenerateDescription()
{
return $"Customer name: {_customer.Name}, " +
$"total number of products: {_products.Count}, " +
$"total price: {GetPrice()}";
}

private decimal GetPrice()
{
decimal basePrice = /* Calculate based on _products */;
decimal discounts = /* Calculate based on _customer */;
decimal taxes = /* Calculate based on _products */;
return basePrice - discounts + taxes;
}
}

可以看到GetPrice函数是非常复杂的,如果对Order类的公开方法进行测试不足以让我们对这个函数有足够的信心,那么有些人可能会尝试将该方法的声明从private改为public。

但生产代码才是一等公民,UT是用来保证生产代码的正确性的,如果从业务的角度,GetPrice函数只是一个实现细节,我们就不应该将其公开,否则不但测试代码会和Order类产生耦合,其他生产代码也可能会和Order类产生不必要的耦合。

有些聪明的同学会说确实不应该仅仅为了测试本身而修改生产代码,那我就用反射机制来测试私有的GetPrice函数总可以了吧?

反射机制确实比直接把GetPrice改为public要好一些,但是这又绕回了文章开头提到的测试代码和生产代码耦合的问题:要么测试代码维护成本很高,要么测试产生大量的误报错。

看起来我们陷入了一个死胡同:私有函数很复杂很重要,仅仅通过类的公开行为对其进行间接测试不足以让我们对该函数有足够信心,如果直接对该函数UT,我们的测试代码又和实现细节产生了耦合。

等等,让我们仔细想一下,这是一个真问题吗?

为什么通过类的公开行为对其进行间接测试不足以让我们对该函数有足够信心?私有函数只是该类的一个实现细节,如果该类的公开行为全部通过测试,我们为什么还要担心呢?

很多时候,这种担心非常可能是一个信号:该私有函数的复杂度超越了其所在的类,只有结合另外一些场景,我们才能对该函数的执行路径进行充分的覆盖,这提示我们该私有函数实际应该被提取成一个单独的类,这个类不但可以被该函数目前所在类使用,还可以在更多场景被其他的类所使用。

有反对者可能会说,这其实和把该函数声明为public也没啥区别,只是换了个形式而已。如前所述,生产代码才是一等公民,测试代码是为了保证生产代码的正确性而存在的。这里把一个复杂的私有函数提取成一个类,其根本目的不是为了更好测试,而是我们发现该函数的逻辑能应用到更多的场景,被更多的类所复用,所以这是一种设计改进。

如Vlad所说:"Code design always comes first. Unit tests merely help you reveal issues with that design",即使我们不做UT,把例子中GetPrice这样的复杂函数提取成一个单独的类也是一个好的设计,UT只是帮我们发现了这一点。



Tag: 测试
相關內容
歡迎評論
未登錄,
請先 [ 註冊 ] or [ 登錄 ]
(一分鍾即可完成註冊!)
返回首頁     ·   返回[討論交流]   ·   返回頂部