仅可测试性是依赖注入的理由吗?

Posted

技术标签:

【中文标题】仅可测试性是依赖注入的理由吗?【英文标题】:Is testability alone justification for dependency injection? 【发布时间】:2011-02-28 11:16:14 【问题描述】:

据我所知,DI 的优势在于:

减少的依赖关系 更多可重用代码 更多可测试代码 更具可读性的代码

假设我有一个存储库 OrderRepository,它充当通过 Linq to Sql dbml 生成的 Order 对象的存储库。我无法使我的订单存储库通用,因为它执行 Linq Order 实体和我自己的 Order POCO 域类之间的映射。

由于 OrderRepository 必然依赖于特定的 Linq to Sql DataContext,因此不能真正说 DataContext 的参数传递使代码可重用或以任何有意义的方式减少依赖关系。

这也使代码更难阅读,因为要实例化我现在需要编写的存储库

new OrdersRepository(new MyLinqDataContext())

这也与存储库的主要目的相反,即从消费代码中抽象/隐藏 DataContext 的存在。

所以总的来说,我认为这将是一个非常糟糕的设计,但它会带来促进单元测试的好处。这是足够的理由吗?还是有第三种方法?我很想听听意见。

【问题讨论】:

【参考方案1】:

关于在您的问题中使用 DI 的问题,我仍然没有定论。您已经问过单独测试是否是实施 DI 的理由,而我在回答这个问题时听起来有点像护卫者,尽管我的直觉回答是不。

如果我回答是,我正在考虑在您没有任何东西可以轻松直接测试时测试系统。在物理世界中,包含端口、访问隧道、接线片等的情况并不少见,以便提供一种简单直接的方法来测试系统、机器等的状态。在大多数情况下,这似乎是合理的。例如,一条输油管道提供了检查舱口,以便将设备注入系统以进行测试和维修。这些是专门构建的,不提供其他功能。但真正的问题是这种范式是否适合软件开发。我的一部分想说是的,但答案似乎是有代价的,让我们处于平衡收益与成本的可爱灰色地带。

“否”论点实际上归结为设计软件系统的原因和目的。 DI 是一种促进代码松散耦合的美丽模式,我们在 OOP 课程中学到的东西是提高代码可维护性的非常重要且强大的设计理念。问题是,像所有工具一样,它可能会被滥用。我将部分不同意 Rob 的上述回答,因为 DI 的优势主要不是测试,而是促进松散耦合架构。而且我认为,仅根据测试能力来设计系统表明在这种情况下要么架构存在缺陷,要么测试用例配置不当,甚至可能两者兼而有之。

在大多数情况下,设计良好的系统架构本质上是易于测试的,而在过去十年中引入的模拟框架使测试变得更加容易。经验告诉我,任何我发现难以测试的系统在某些方面都存在过于紧密的耦合。有时(更罕见)这已被证明是必要的,但在大多数情况下并非如此,而且通常当一个看似简单的系统似乎太难测试时,这是因为测试范式存在缺陷。我已经看到 DI 被用作规避系统设计的一种手段,以允许对系统进行测试,并且风险肯定超过了预期的回报,系统架构实际上被破坏了。我的意思是代码的后门导致安全问题,代码膨胀,运行时从未使用过的特定于测试的行为,以及源代码的意大利面化,以至于你需要几个夏尔巴人和一个占卜板才能弄清楚哪个方法好了!所有这些都在交付的生产代码中。由此产生的维护、学习曲线等成本可能是天文数字,对于小公司来说,从长远来看,这种损失可能是毁灭性的。

恕我直言,我不认为 DI 应该被用作简单地提高代码可测试性的手段。如果 DI 是您进行测试的唯一选择,那么通常需要重构设计。另一方面,通过设计实现 DI,它可以用作运行时代码的一部分,可以提供明显的优势,但不应该被滥用,因为它也可能导致类被滥用,也不应该过度使用。使用它只是因为它看起来很酷且简单,因为在这种情况下它会使您的代码设计过于复杂。

:-)

【讨论】:

【参考方案2】:

依赖注入只是一种达到目的的手段。这是enable loose coupling的一种方式。

【讨论】:

【参考方案3】:

这里的测试设计总是依赖于 SUT(被测系统)。问自己一个问题——我想测试什么?

如果您的存储库只是数据库的访问者 - 那么需要像访问者一样对其进行测试 - 涉及数据库。 (其实这种测试不是单元测试而是集成)

如果您的存储库执行一些映射或业务逻辑并充当数据库的访问者,那么就需要进行分解以使您的系统符合 SRP (单一职责原则)。分解后,您将拥有 2 个实体:

    OrdersRepository OrderDataAccessor

彼此分开测试,打破与 DI 的依赖关系。

关于构造函数的丑陋...使用 DI 框架来构造你的对象。例如,使用 Unity 您的构造函数:

var repository = new OrdersRepository(new MyLinqDataContext());

看起来像:

var repository = container.Resolve<OrdersRepository>;

【讨论】:

【参考方案4】:

我发现需要依赖注入容器和可测试性之间的关系是相反的。我发现 DI 非常有用因为我编写了可单元测试的代码。而且我编写可单元测试的代码是因为它本质上是更好的代码——更松散耦合的更小的类。

IoC 容器是工具箱中的另一个工具,可帮助您管理应用程序的复杂性 - 它运行良好。我发现它可以通过从图片中取出实例来更好地对接口进行编码。

【讨论】:

【参考方案5】:

依赖注入的美妙之处在于能够隔离您的组件

隔离的一个副作用是更容易进行单元测试。另一个是为不同环境交换配置的能力。另一个是每个类的自我描述性质。

无论您如何利用 DI 提供的隔离,您都拥有比紧密耦合的对象模型更多的选择。

【讨论】:

【参考方案6】:

依赖注入的主要优势在于测试。当我第一次开始采用测试驱动开发和 DI 时,您发现了一些对我来说似乎很奇怪的东西。 DI 确实打破了封装。单元测试应该测试与实现相关的决策;因此,您最终会暴露在纯粹封装的场景中不会暴露的细节。您的示例是一个很好的示例,如果您不进行测试驱动开发,您可能希望封装数据上下文。

但是你说,由于 OrderRepository 必然依赖于特定的 Linq to Sql DataContext,我不同意 - 我们有相同的设置并且只依赖于一个接口。你必须打破这种依赖。

但是,如果您的示例更进一步,您将如何在不使用数据库的情况下测试您的存储库(或它的客户端)?这是单元测试的核心原则之一——您必须能够在不与外部系统交互的情况下测试功能。没有什么比数据库更重要的了。依赖注入是一种能够打破对子系统和层的依赖的模式。没有它,单元测试最终需要大量的夹具设置,变得难以编写、脆弱并且太慢了。结果 - 你不会写它们。

让你的例子更进一步,你可能有

在单元测试中:

// From your example...

new OrdersRepository(new InMemoryDataContext());

// or...

IOrdersRepository repo = new InMemoryDataContext().OrdersRepository;

在生产中(使用 IOC 容器):

// usually...

Container.Create<IDataContext>().OrdersRepository

// but can be...

Container.Create<IOrdersRepository>();

(如果您还没有使用 IOC 容器,它们是使 DI 工作的粘合剂。将其视为对象图的“make”(或 ant)...容器为您构建依赖关系图并完成所有繁重的建筑工作)。在使用 IOC 容器时,您可以取回您在 OP 中提到的依赖项隐藏。依赖项由容器作为单独的关注点进行配置和处理 - 调用代码可以只请求接口的实例。

有一本非常优秀的书详细探讨了这些问题。查看 Mezaros 的 xUnit 测试模式:重构测试代码。这是将您的软件开发能力提升到新水平的书籍之一。

【讨论】:

DI 的主要优势在于解耦。来自en.wikipedia.org/wiki/Dependency_inversion_principle 的引用:“依赖倒置原则的目标是将高级组件与低级组件解耦,从而可以重用不同的低级组件实现。” 感谢您提供的信息丰富的答案。我很困惑你说 OrdersRepository 不必依赖于特定的 DataContext。根据定义,检索 Order Linq 实体的任何内容都依赖于公开该实体的特定 DataContext。如果我将 OrdersRepository 实例化为使用其设计工作的特定 DataContext 以外的任何内容,则该代码将失败。所以事实上,我想我可以在 OP 中提出的另一点是,使用 DI 似乎会损害代码的安全性/稳健性。 我还应该指出,我稍微简化了 OP 中的示例。我所拥有的更准确的图片是OrdersRepository -&gt; IRepository&lt;Order&gt; -&gt; Linq DataContext,即new OrdersRepository(new Repository&lt;Order&gt;(new MyDataContext())) 所以我有一个通用存储库,通过 OrdersRepository 包装的接口公开。但是 OrdersRepository 的非常具体的映射角色(从 Linq 实体到域实体)是这样的,以至于我看不出通过这里的接口公开的通用版本如何可行或有用。 @fearofawhackplanet 关于 DataContext 依赖(如何):LINQ to SQL 通过 IQueryable 工作并返回 POCO 实体对象。 POCO 是生成的,但可以通过部分类进行扩展(即,您不需要映射到单独的域对象)。您可以通过将 POCO 保存在集合中并返回 .AsQueryable() 来打破依赖关系。我从我们的环境中删除了一些文件并将它们留在drop.io/TestableDataContext 上,这显示了一种方法。它们不会孤立地编译,但如果你接受下一条评论(为什么),它们可以让你了解如何处理这个问题。 @fearofawhacplanet 关于有用性(为什么):您打破了依赖关系,因此您可以在不需要数据库的情况下对 DataContext 进行单元测试。如果这不是目标,我同意在您的场景中不值得使用 DI。【参考方案7】:

小注释先:依赖注入 = IoC + 依赖倒置。对于测试和您实际描述的最重要的是依赖关系inversion

一般来说,我认为测试证明依赖倒置是合理的。但这并不能证明依赖注入是合理的,我不会为了测试而引入 DI 容器。

但是,依赖倒置是一个可以在必要时稍微弯曲的原则(就像所有原则一样)。您可以特别在某些地方使用工厂来控制对象的创建。

如果你有 DI 容器,它会自动发生; DI 容器充当工厂并将对象连接在一起。

【讨论】:

【参考方案8】:

当您使用诸如 StructureMap 之类的控制反转容器时,依赖注入的威力就来了。使用它时,您不会在任何地方看到“新”——容器将控制您的对象构造。这样一来,一切都不会意识到依赖关系。

【讨论】:

【参考方案9】:

可以用 Java 编写通用数据访问对象:

package persistence;

import java.io.Serializable;
import java.util.List;

public interface GenericDao<T, K extends Serializable>

    T find(K id);
    List<T> find();
    List<T> find(T example);
    List<T> find(String queryName, String [] paramNames, Object [] bindValues);

    K save(T instance);
    void update(T instance);
    void delete(T instance);

我不能代表 LINQ,但 .NET 有泛型,所以应该可以。

【讨论】:

是的,这种模式也普遍适用于 .NET,但在我的示例中,Orders 对象仅作为 Linq DataContext 的属性公开。所以重点是OrdersRepository已经对具体的DataContext有依赖了,不管是否使用DI。

以上是关于仅可测试性是依赖注入的理由吗?的主要内容,如果未能解决你的问题,请参考以下文章

详解Spring IoC容器

.NET 通过源码深究依赖注入原理

如何解决 ASP.NET Core 中的依赖问题

WPF 应用程序是不是可以进行依赖注入?

可测试性战术分析

当您还需要“默认”具体依赖项时,哪个是更好的依赖注入模式?