部分模拟很糟糕,为什么呢?

脚本

我有一个类称为Model ,它表示不同类型的许多其他对象的复杂的复合对象。 您可以将其视为具有Door[]Tire[]EngineDriver等的Car 。而这些对象又具有子对象,例如Engine具有SparkPlugClutchGenerator等。

我有一个Metrics类,它可以计算一些或多或少复杂的关于Model指标,实质上它看起来像这样:

 public class Metrics{ private final Model model; public Metrics(Model aModel){model = aModel;} public double calculateSimpleMetric1(){...} public double calculateSimpleMetric2(){...} public double calculateSimpleMetricN(){...} public double calculateComplexMetric(){ /* Function that uses calls to multiple calculateSimpleMetricX to calculate a more complex metric. */ } } 

我已经为calculateSimpleMetricX函数编写了测试,并且每个函数都需要非常平凡但可管理的数量(10-20行)的设置代码来正确模拟模型的相关部分。

问题

由于Model类的不可避免的复杂性,存根/ Model computeComplexMetric calculateComplexMetric()所有依赖关系会产生一个非常大且难以维护的测试(超过100行的设置代码来测试合理的代表性场景,我需要测试相当的几个场景)。

我的想法是创建一个Model的部分模拟,并存根相关的calculateSimpleMetricX()函数,以将设置代码减少到可管理的10行代码。 由于这些function已经单独测试。

然而,Mockito文档指出, 部分嘲笑是一种代码气味

个人会在这里切角,只是部分模仿Metrics类。 但我有兴趣了解构建此代码和unit testing的“纯粹主义”方式是什么?

我最终按照接受的答案和使用dependency injection的方式分成更小,更有凝聚力的类:

 public class AbstractMetric{ abstract public double calculate(); } public class ComplexMetric extends AbstractMetric{ private final SimpleMetric1 sm1; private final SimpleMetric2 sm2; public ComplexMetric(SimpleMetric1 asm1, SimpleMetric2 asm2){ sm1 = asm1; sm2 = asm2; } @Ovrerride public double calculate(){ return functionof(sm1.calculate(), sm2.calculate()); } } 

它是可测试的,如果需要在稍后阶段提高性能,我可以让AbstractMetric每个实现都实现缓存。 我发现将指标添加到构造函数是可以接受的,相比之下,在每次调用中将它们传递给calculate函数。 即使这可以用工厂方法隐藏。

我猜这背后的推理可以如下:如果你需要部分模拟一个类来测试忽略其行为的一部分的东西,那么这个类做的不止一件事。 这违反了单一责任原则 ,这就是代码气味。

此外,如果一个对象可以对其内部部分存根(因此,实际上不存在)做任何有用的事情,那么这个对象具有低内聚力,这也是一种代码气味。

您的Metric类不仅仅是一件事 – 它会计算不同类型的度量标准。 考虑将其重构为抽象Metric和适当的子类 – 每个子类都计算自己的度量标准。

有关部分模拟效用的一些论据可以在这里找到。 然而,它们都建立在SRP已经被违反的情况下,你无法做任何事情。

由于Model类的不可避免的复杂性,存根/模拟computeComplexMetric()的所有依赖关系会产生一个非常大且难以维护的测试(超过100行的设置代码来测试合理的代表性场景,我需要测试相当的几个场景)。

谁说你需要模拟/存储calculateComplexMetric()所有依赖项呢? 你问的是纯粹的测试方法。 如果你不是指模仿者的方式,那么通过模拟你的核心应用程序代码的“端口”(UI,数据库等)并将几个类unit testing在一起。 如果您的应用程序已经拥有基于较少量设置构建实际Model的代码,这可以解决您的问题。 如果可能的话,我更愿意这样做。

否则,如果你想把这些方法放在这个类中(这可能很有意义),你可以做一些可能违背每个人建议的事情(当然不是纯粹主义 ),但如果calculateComplexMetric()可以完全没问题。在给定简单度量的结果的情况下,可以以完全无状态的方式实现:创建一个static方法(可以是公共的,甚至只是包可见而不需要在同一个类中),使用简单的度量来实现计算参数。 这应该让它真的很容易测试。 原始的calculateComplexMetric()只调用简单方法并委托给静态方法。 它的实现可以非常简单,不需要单独进行显式测试。

这种“缺点”是静态方法更难以模拟。 但这不应该是必要的,因为它不跨越任何端口(例如,访问DB或调用Web服务)。 但无论如何,这可能与你能得到的纯粹主义相去甚远。

最后,您可以考虑将其添加为Metrics类中的常规方法,而不是将其设置为静态方法。 如果您不想公开它并且您的测试位于同一个包中,您甚至可以将其打包为私有。

我们在谈论几个calculateSimpleMetricX方法? 如果它只是少数,也许值得考虑将简单度量的返回值作为参数传递给calculateComplexMetric

 public double calculateComplexMetric(double simple1, double simple2, ...) 

如果参数太多,可能需要将简单度量的值封装到新类SimpleMetrics并将其传递给:

 public double calculateComplexMetric(SimpleMetrics metrics){ double sm1 = metrics.get1(); double sm2 = metrics.get2(); ... } 

无论哪种方式,现在都可以轻松测试复杂的度量计算,因为您在没有模拟的情况下提供了简单度量的值。