伪装接口或虚拟方法的依赖是一个好的设计选择吗?

Is faking dependencies with interfaces or virtual methods a good design choice?

我正在尝试学习单元测试,但它导致了一个设计问题。考虑 class A 依赖于 class B。如果要为 B 创建存根以便对 A 进行单元测试,大多数隔离框架要求 B 必须是接口或使用的所有方法by A 必须是虚拟的。 B 本质上不能是具有非虚拟方法的具体 class 以进行单元测试。

这对生产代码的设计施加了重大限制。如果我必须为每个依赖项创建一个接口,那么 classes 的数量将加倍。遵循单一职责原则会导致相互依赖的小 classes,因此这会增加接口的数量。此外,我还为易失性依赖项(将来可能会发生变化)或设计需要它以实现可扩展性创建接口。使用仅用于测试的接口污染生产代码将显着增加其复杂性。 使所有方法虚拟化似乎也不是一个好的解决方案。它给继承者的印象是这些方法可以被覆盖,即使它们不是,实际上这只是单元测试的副作用。

这是否意味着可测试的面向对象设计不允许具体的依赖关系,或者这是否意味着不应伪造具体的依赖关系? "Every dependency must be faked(stub or mock) to unit test correctly" 是我到目前为止学到的,所以我认为后者不是这种情况。除了 JustMock 和 Isolator 之外的隔离框架不允许在没有虚拟方法的情况下伪造具体的依赖关系,一些人认为 JustMock 和 Isolator 的强大功能会导致糟糕的设计。我认为模拟任何 class 的能力非常强大,如果您知道自己在做什么,它将使生产代码的设计保持整洁。

后来才知道这个question也问了同样的问题,好像没有解决办法。在创建接口或使所有方法虚拟化之间进行选择是 C# 的一个限制,它是一种静态类型的语言。 Ruby 等 Duck 类型的语言不会强加这一点,并且可以在不更改原始 class 的情况下轻松创建假对象。在Ruby中,假对象只需要创建适当的方法,就可以用来代替原来的依赖。

编辑:

我读完了 Roy Osherove 的《The Art of Unit Testing》一书,发现以下段落是相关的:

Testable designs usually only matter in static languages, such as C# or VB.NET, where testability depends on proactive design choices that allow things to be replaced. Designing for testability matters less in more dynamic languages, where things are much more testable by default. In such languages, most things are easily replaceable, regardless of the project design. This rids the community of such languages from the straw-man argument that the lack of testability of code means it’s badly designed and lets them focus on what good design should achieve, at a deeper level.

Testable designs have virtual methods, nonsealed classes, interfaces, and a clear separation of concerns. They have fewer static classes and methods, and many more instances of logic classes. In fact, testable designs correlate to SOLID design principles but don’t necessarily mean you have a good design. Perhaps it’s time that the end goal should not be testability but good design alone.

这基本上意味着由于静态语言的限制而使设计可测试并不能使它本身成为 "good design"。对我来说,一个好的设计可以满足当今的需求,而不会过多地考虑未来。当然,使每个依赖项都抽象化有利于将来的可维护性,但它会使 API 变得非常复杂。如果它可能会更改或许多具体 classes 实现该接口不是因为可测试性需要它,我想将依赖项作为接口。这样做是因为可测试性需要它导致 "bad design"。