【问题标题】:How to deal with interface overuse in TDD?如何处理 TDD 中的接口过度使用?
【发布时间】:2011-07-21 15:56:53
【问题描述】:

我注意到,当我进行 TDD 时,它通常会导致大量的接口。对于具有依赖关系的类,它们通过构造函数以通常的方式注入:

public class SomeClass
{
    public SomeClass(IDependencyA first, IDependency second)
    {
        // ...
    }
}

结果是几乎每个类都会实现一个接口。

是的,代码将被解耦,并且可以很容易地单独测试,但也会有额外的间接级别,这让我感到有点……不安。感觉有些不对劲。

谁能分享其他不涉及大量使用接口的方法?

你们其他人还好吗?

【问题讨论】:

  • 不确定这个问题是否可以按现状回答......我看到接口是实际实现/类需要满足的角色。例如为了让“我”“快速到达办公室”,我需要一个“交通工具”和“最佳路径查找器”。接口也是分隔 2 层的边界。因此,这些界面可以帮助您确定需要哪些角色,并为他们分配明确的离散职责。如果您想单独测试每个类(没有它的依赖项),则看不到避免它的方法;不过,您可能需要重新审视自己的角色和职责

标签: c# unit-testing interface tdd


【解决方案1】:

你的测试告诉你重新设计你的类。

有时您无法避免传递需要存根以使您的类可测试的复杂协作者,但您应该寻找方法为他们提供这些协作者的输出,并且考虑如何重新安排它们的交互以消除复杂的依赖关系。

例如,不要为TaxCalculator 提供ITaxRateRepository(在CalculateTaxes 期间访问数据库),而是在创建TaxCalculator 实例之前获取这些值并将它们提供给它的构造函数:

// Bad! (If necessary on occasion)
public TaxCalculator(ITaxRateRepository taxRateRepository) {}

// Good!
public TaxCalculator(IDictonary<Locale, TaxRate> taxRateDictionary) {}

有时这意味着您必须进行更大的更改、调整对象的生命周期或重组大量代码,但是一旦我开始寻找它,我经常会发现低洼的果实。

有关减少对依赖项的依赖的优秀技术综述,请参阅Mock Eliminating Patterns

【讨论】:

  • 我真的很喜欢你发布的链接。
【解决方案2】:

不要使用接口!大多数模拟框架都可以模拟具体的类。

【讨论】:

  • 你是说我应该注入具体的类而不是接口?
  • 为什么不呢?如果您创建它的唯一原因是为了模拟它,那么在生产代码中创建一个只有一个实现的接口就没有什么意义了。
  • 所以我应该将这些具体类中的方法设为虚拟以模拟它们,是这样吗?
  • 您没有特别说您使用的是 Java,但既然您提到了接口,我就假设您是。在这种情况下,Java 中的所有(非静态)方法都已经是虚拟的了。
  • 在 c# 中,声明虚拟方法比创建冗余接口要好。见stackoverflow.com/questions/90851/…
【解决方案3】:

这是基于模拟的测试方法的缺点。这既是关于模拟的讨论,也是关于测试边界的讨论。通过测试用例与域类的比例为 1:1,您的测试边界非常小。小测试边界的结果是依赖于它们的接口和测试的激增。由于您要模拟和排除的交互数量,重构变得更加困难。通过使用单个测试来测试类集群,重构变得更容易并且您使用的接口更少。但是请注意,您可以一次测试太多类。你的类越复杂,你需要测试的代码路径就越多。这可能导致组合爆炸,您不可能全部测试它们。听代码和测试,他们告诉你一些关于你的代码的事情。如果您发现复杂性在增加,那么现在可能是引入新的测试用例和接口/实现并在您的原始版本中模拟出来的好时机。

【讨论】:

  • 如果我在一个测试中测试多个类,那么我认为这些类应该与相同的功能相关。一个例子是 A 类使用 B 类 + C 类,并且两者都是辅助类。我应该将 B 和 C 注入 A 还是让 A 实例化它们?
  • 我的风格是在构造函数中实例化它们,这样如果您决定稍后单独测试 A、B 和 C 类,您可以通过简单的 Introduce Parameter 重构使它们可注入。 DI 是个好东西,但它仍然可以是 YAGNI。
  • 您的考虑是关于何时使用模拟/存根。它并不能证明模拟需要接口。见stackoverflow.com/questions/90851/…
【解决方案4】:

如果您对传递给特定类的接口数量感到不安;那么这可能表明您引入了太多不同的依赖项。

如果 SomeClass 依赖于 IDependencyAIDependencyBIDependencyC,这是一个机会来看看你是否可以将类使用这三个接口执行的逻辑提取到另一个类/接口,IDependencyABC

那么当你为 SomeClass 编写测试时,你只需要模拟出 IDependencyABC 现在提供的逻辑。

另外,如果你还是不舒服;也许它不是您需要的界面。例如,包含状态的类(例如,传递的参数)可能只是被创建并作为具体类传递。杰夫的回答暗示了这一点,他提到只将你需要的东西传递给你的构造函数。这减少了您的构造之间的耦合,并且更好地表明了您的类需求的意图。请小心传递数据结构(IDictionary)。

最终,当您在周期中获得那种温暖的模糊感时,TDD 就起作用了。如果您感到不安,请注意一些代码异味并修复其中一些问题以重回正轨。

【讨论】:

  • "请小心传递数据结构 (IDictionary)。" - 似乎你在反对杰夫在他的例子中提出的建议。你能解释一下为什么传递数据结构会不好吗?
  • 在类内部传递基本数据结构通常没有什么问题。我在公共接口中发现了这一点;然而,这可以掩盖方法调用的意图。 可能更适合传入模拟所需内容的类(并且它可能在内部有一个字典)。我曾多次使用传递哈希集、字典和列表的许多方法来调试遗留代码。所有这些的意图都非常不清楚,因为基本数据结构所固有的上下文很少。
  • 好的,现在我明白你的意思了。我同意,适当的域对象通常比原始数据结构更好。
  • 不要拖拖拉拉,但原始数据结构(字典、列表、哈希集)是可变的。在传递可变结构时,这可能会导致很多调试问题。
猜你喜欢
  • 1970-01-01
  • 2011-03-01
  • 1970-01-01
  • 2013-03-31
  • 1970-01-01
  • 2010-09-14
  • 1970-01-01
  • 2011-05-15
  • 1970-01-01
相关资源
最近更新 更多