安全地对 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
(而不是 RootFolderFilePathProvider
或 NCrunchFilePathProvider
在测试中使用)基于告诉你它做了什么:)
有几个替代配置:
BindToDefaultInterfaces
(注意复数)将 MyOtherBar
绑定到 IMyOtherBar
、IOtherBar
和 IBar
(我认为)
BindToSingleInterface
如果每个 class 都恰好实现 1 个接口,则 有效。
BindToAllInterfaces
言出必行。
我想更改为那些,但我担心引入模糊的错误,一些 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 接口,并确保每个接口都可以被内核解析。
这种方法有几个问题:
- 如果您错过了一个程序集,或者有人添加了一个新程序集,但没有将其添加到测试中怎么办?
这不是直接的问题,但这意味着您的测试并没有像您想象的那样保护您。我进行了安全网测试,以断言 Ninject 内核知道的每个程序集都应该在要测试的程序集列表中。如果有人添加一个新的程序集,它很可能包含内核提供的东西,所以这个安全网测试就会失败,引起开发人员对这个测试的关注class .
- 内核未提供的 classes 怎么办?
我发现主要是这些 classes 没有提供一个明确的原因 - 也许它们实际上是由 Factory classes 提供的,或者可能 class 使用不当并且是人工建造的。无论哪种方式,这些 classes 都是少数,可以相对轻松地列为显式例外 ("loop over all classes; if classname = foo then ignore it.")。
总的来说,这是适度的多毛。而且比我通常希望的测试更脆弱。
但它有效。
它可能是您在进行更改之前写的东西,只是为了让您可以 运行 在更改之前写一次,在更改之后检查一次是否有问题,然后将其删除?
具体问题: 我如何根据我的代码库对我的 DI 配置进行单元测试,以确保在我对自动绑定检测进行一些更改后所有接线仍然有效。
我一直在为一个小型代码库(可能约 10 页?和 20-30 services/controllers)做贡献,它使用 Ninject 作为 Ioc/DI。
我发现在 Ninject 内核中它被配置为 BindDefaultInterface
。这意味着如果你向它请求 IFoo,它会去寻找 Foo class.
但它是基于字符串模式而不是 C# 继承来实现的。这意味着 MyFoo : IFoo
不会绑定,你也可能会得到其他奇怪的 "coincidental" 绑定,也许吧?
到目前为止一切正常,因为每个人都恰好调用了他们的 WhateverService 接口 IWhateverService
。
但这对我来说似乎非常脆弱和不直观。当我想将我的 live FilePathProvider : IFilePathProvider
重命名为 AppSettingsBasedFilePathProvider
(而不是 RootFolderFilePathProvider
或 NCrunchFilePathProvider
在测试中使用)基于告诉你它做了什么:)
有几个替代配置:
BindToDefaultInterfaces
(注意复数)将MyOtherBar
绑定到IMyOtherBar
、IOtherBar
和IBar
(我认为)BindToSingleInterface
如果每个 class 都恰好实现 1 个接口,则 有效。
BindToAllInterfaces
言出必行。
我想更改为那些,但我担心引入模糊的错误,一些 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 接口,并确保每个接口都可以被内核解析。
这种方法有几个问题:
- 如果您错过了一个程序集,或者有人添加了一个新程序集,但没有将其添加到测试中怎么办?
这不是直接的问题,但这意味着您的测试并没有像您想象的那样保护您。我进行了安全网测试,以断言 Ninject 内核知道的每个程序集都应该在要测试的程序集列表中。如果有人添加一个新的程序集,它很可能包含内核提供的东西,所以这个安全网测试就会失败,引起开发人员对这个测试的关注class .
- 内核未提供的 classes 怎么办?
我发现主要是这些 classes 没有提供一个明确的原因 - 也许它们实际上是由 Factory classes 提供的,或者可能 class 使用不当并且是人工建造的。无论哪种方式,这些 classes 都是少数,可以相对轻松地列为显式例外 ("loop over all classes; if classname = foo then ignore it.")。
总的来说,这是适度的多毛。而且比我通常希望的测试更脆弱。
但它有效。
它可能是您在进行更改之前写的东西,只是为了让您可以 运行 在更改之前写一次,在更改之后检查一次是否有问题,然后将其删除?