C# 如何处理TDD中的接口过度使用?

C# 如何处理TDD中的接口过度使用?,c#,unit-testing,interface,tdd,C#,Unit Testing,Interface,Tdd,我注意到,当我做TDD时,它通常会导致大量的接口。对于具有依赖项的类,它们以通常的方式通过构造函数注入: public class SomeClass { public SomeClass(IDependencyA first, IDependency second) { // ... } } 结果是几乎每个类都将实现一个接口 是的,代码将是解耦的,并且可以很容易地进行隔离测试,但是也会有额外的间接层次,这让我感到有点……不安。有点不对劲 有人能分享其他

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

public class SomeClass
{
    public SomeClass(IDependencyA first, IDependency second)
    {
        // ...
    }
}
结果是几乎每个类都将实现一个接口

是的,代码将是解耦的,并且可以很容易地进行隔离测试,但是也会有额外的间接层次,这让我感到有点……不安。有点不对劲

有人能分享其他不涉及如此大量使用接口的方法吗


你们其他人怎么样了?

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

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

您的测试告诉您重新设计类

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

例如,在创建
TaxCalculator
实例之前获取这些值,然后将其提供给构造函数,而不是使用
ITaxRateRepository
提供
TaxCalculator
(在
CalculateTaxes
期间访问数据库):

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

// Good!
public TaxCalculator(IDictonary<Locale, TaxRate> taxRateDictionary) {}
//糟糕!(如有时需要)
公共TaxCalculator(ITaxRateRepository taxRateRepository){}
//好!!
公共税收计算器(国际税收词典){}
有时这意味着你必须做出更大的改变,调整对象的生命周期或重组大量的代码,但我经常在开始寻找它的时候发现一些低效的结果


有关减少依赖关系的技术的优秀综述,请参阅。

如果您对传递到特定类中的接口数量感到不安;那么,这可能是一个迹象,表明您引入了太多不同的依赖项

如果某个类依赖于IDependencyA、IDependencyB和IDependencyC,那么这是一个机会,让您看看是否可以将该类使用这三个接口执行的逻辑提取到另一个类/接口IDependencyABC中

然后,当您为某个类编写测试时,您只需要模拟IDependencyABC现在提供的逻辑


此外,如果你仍然不舒服;也许这不是您需要的接口。例如,包含状态的类(例如,传递的参数)可能只是创建并作为具体类传递。Jeff的回答暗示了这一点,他提到只将您需要的内容传递给构造函数。这减少了构造之间的耦合,更好地表明了类需求的意图。只是要小心传递数据结构(IDictionary)


最后,当你在你的周期中得到温暖的模糊感觉时,TDD就起作用了。如果您感到不安,请注意一些代码的气味,并解决其中一些问题,以便回到正轨。

您是说我应该注入具体的类而不是接口吗?为什么不?如果创建一个在生产代码中只有一个实现的接口的唯一原因是为了可以对其进行模拟,那么创建该接口就没有什么意义了。因此,我应该将这些具体类中的方法虚拟化,以便对其进行模拟,是吗?您没有明确说过您正在使用Java,但既然你提到了接口,我就认为你是。在这种情况下,Java中的所有(非静态)方法都是虚拟的。在c#中,声明虚拟方法比创建冗余接口更好。请参阅“小心传递数据结构(IDictionary)。”——似乎您在反对Jeff在其示例中的建议。你能解释一下为什么传递数据结构是不好的吗?一般来说,在类内部传递基本数据结构并没有什么错。我发现在公共接口中;但是,这可能会掩盖方法调用的意图。在类中传递所需的模型可能更合适(并且它内部可能有一个字典)。我不止一次不得不调试遗留代码,使用许多方法传递哈希集、字典和列表。所有这些的意图都非常不清楚,因为基本数据结构中存在最小的固有上下文。我同意,一个合适的域对象通常比一个基本的数据结构要好。不要拖到这里,但基本的数据结构(字典、列表、哈希集)是可变的。在传递可变结构时,这可能会导致很多调试问题。如果我在一个测试中测试几个类,那么我认为这些类应该与同一个特性相关。一个例子是,类A使用类B+C,两者都是助手类。我应该注射吗