安全地对 IoC/DI 配置进行广泛的更改

Safely making wide-reaching change to IoC/DI config

具体问题: 我如何根据我的代码库对我的 DI 配置进行单元测试,以确保在我对自动绑定检测进行一些更改后所有接线仍然有效。


我一直在为一个小型代码库(可能约 10 页?和 20-30 services/controllers)做贡献,它使用 Ninject 作为 Ioc/DI。

我发现在 Ninject 内核中它被配置为 BindDefaultInterface。这意味着如果你向它请求 IFoo,它会去寻找 Foo class.

但它是基于字符串模式而不是 C# 继承来实现的。这意味着 MyFoo : IFoo 不会绑定,你也可能会得到其他奇怪的 "coincidental" 绑定,也许吧?

到目前为止一切正常,因为每个人都恰好调用了他们的 WhateverService 接口 IWhateverService

但这对我来说似乎非常脆弱和不直观。当我想将我的 live FilePathProvider : IFilePathProvider 重命名为 AppSettingsBasedFilePathProvider(而不是 RootFolderFilePathProviderNCrunchFilePathProvider 在测试中使用)基于告诉你它做了什么:)

有几个替代配置:

我想更改为那些,但我担心引​​入模糊的错误,一些 class 某处停止以应有的方式绑定,但我没有注意到。

有没有什么方法可以在合理的安全性(即超过 "do it and hope",无论如何!)的情况下测试这个/做出这个改变,而不只是试图弄清楚如何锻炼每个可能的组件。

所以,我设法解决了这个... 我的方案并非没有缺点,但确实从根本上达到了我想要的安全性。


总结

大致来说有2个方面:

  • 以编程方式测试 DI 内核知道的每个绑定都可以干净地解析。
  • 以编程方式测试代码库中使用的每个相关接口都可以干净地解析。

两者大致相同:

  • 重构您的 DI 配置代码,以便它的核心部分为您的应用定义绑定,可以 运行 与启动代码的其余部分隔离。
  • 在你的测试开始时调用上面的 DI 配置代码,这样你就有了你的站点使用的内核对象的副本,你可以测试它的绑定
  • 执行一些反射,以生成内核应该能够提供的相关 Type 对象的列表。
  • (可选) 过滤该列表以忽略一些 class 你知道你的测试不需要关心的元素和接口(例如你的代码不需要无需担心内核是否知道如何 bootstrap 本身,因此它可以忽略它在属于您的 DI 框架的名称空间中的任何绑定。)。
  • 然后循环遍历您留下的接口类型对象,并确保 kernel.Get(interfaceType) 运行 每个对象都没有异常。

继续阅读以了解更多血淋淋的细节...


正在验证所有定义的内核绑定

这将特定于所讨论的 DI 框架,但对于 Ninject 来说,它非常复杂...

如果 Ninject 内核具有公开其绑定集合的内置方法,那就更好了,但遗憾的是它没有。但是绑定集合是私有的,所以如果你执行正确的反射咒语,你可以得到它们。然后,您必须进行一些 more 反射,以将其 Binding 对象转换为 {InterfaceType : ConcreteType} 对。

我将 post 分别说明如何从 Ninject 中提取这些对象的细节,因为这与一般情况下如何为 DI 配置设置测试的问题是正交的。 {#Placeholder for a link to that#}

其他 DI 框架可能会通过更 publicly 提供这些集合(或者甚至通过直接提供某种 Validate() 方法来简化此操作。)

一旦你有了内核认为它可以绑定的接口列表,就循环它们并测试解析每个接口。

详细信息因语言和测试框架而异,但我使用 C#FluentAssertions,因此我分配了 Action resolutionAction = (() => testKernel.Get(interfaceType)) 并断言 resolutionAction.ShouldNotThrow() 或非常相似的东西.


验证代码库中的所有相关接口

前半部分一切都很好,但它只告诉您您 DI 拾取的绑定是明确定义的。它不会告诉您是否有任何绑定完全缺失

您可以通过在您的代码库中收集所有有趣的程序集来解决这个问题:

Assembly.GetAssembly(typeof(Main.SampleClassFromMainAssembly))
Assembly.GetAssembly(typeof(Repos.SampleRepoClass))
Assembly.GetAssembly(typeof(Web.SampleController))
Assembly.GetAssembly(typeof(Other.SampleClassFromAnotherSeparateAssemblyInUse))

然后为每个 Assembly 反思它的 classes 以找到它公开的 public 接口,并确保每个接口都可以被内核解析。

这种方法有几个问题:

  1. 如果您错过了一个程序集,或者有人添加了一个新程序集,但没有将其添加到测试中怎么办?

这不是直接的问题,但这意味着您的测试并没有像您想象的那样保护您。我进行了安全网测试,以断言 Ninject 内核知道的每个程序集都应该在要测试的程序集列表中。如果有人添加一个新的程序集,它很可能包含内核提供的东西,所以这个安全网测试就会失败,引起开发人员对这个测试的关注class .

  1. 内核未提供的 classes 怎么办?

我发现主要是这些 classes 没有提供一个明确的原因 - 也许它们实际上是由 Factory classes 提供的,或者可能 class 使用不当并且是人工建造的。无论哪种方式,这些 classes 都是少数,可以相对轻松地列为显式例外 ("loop over all classes; if classname = foo then ignore it.")。


总的来说,这是适度的多毛。而且比我通常希望的测试更脆弱。

但它有效。

它可能是您在进行更改之前写的东西,只是为了让您可以 运行 在更改之前写一次,在更改之后检查一次是否有问题,然后将其删除?