安全地对IoC/DI配置进行广泛的更改 [英] Safely making wide-reaching change to IoC/DI config

查看:71
本文介绍了安全地对IoC/DI配置进行广泛的更改的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

特定问题: 如何在我的代码库中对DI配置进行单元测试,以确保在对自动绑定检测进行一些更改后,所有连接仍能正常进行.

Specific Question: How can I unit Test my DI configuration against my codebase to ensure that all the wiring up still works after I make some change to the automated binding detection.

我一直在为使用Ninject进行Ioc/DI的小型代码库(也许约10页?和20-30个服务/控制器)做出贡献.

I've been contributing to a small-ish codebase (maybe ~10 pages? and 20-30 services/controllers) which uses Ninject for Ioc/DI.

我发现在Ninject内核中它被配置为BindDefaultInterface.这意味着,如果您要求它提供IFoo,它将去寻找Foo类.

I've discovered that in the Ninject Kernel it is configured to BindDefaultInterface. That means that if you ask it for an IFoo, it will go looking for a Foo class.

但是它是基于字符串模式而不是C#继承的.这意味着MyFoo : IFoo将不会绑定,并且您还可能会获得其他奇怪的巧合"绑定,也许是这样?

But it does that based on the string pattern, not the C# inheritance. That means that MyFoo : IFoo won't bind, and you could also get other weird "coincidental" bindings, maybe?

到目前为止,所有操作都有效,因为每个人碰巧都调用了自己的WhateverService接口IWhateverService.

It all works so far, because everyone happens to have called their WhateverService interface IWhateverService.

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

But this seems enormously brittle and unintuitive to me. And it specifically broke when I wanted to rename my live FilePathProvider : IFilePathProvider to be AppSettingsBasedFilePathProvider (as opposed to the RootFolderFilePathProvider, or the NCrunchFilePathProvider which get used in Test) on the basis of that telling you what it did :)

有两种替代配置:

  • BindToDefaultInterfaces(请注意复数),它将把MyOtherBar绑定到IMyOtherBarIOtherBar& IBar(我认为)
  • 如果每个类都只实现一个接口,则
  • BindToSingleInterface起作用.
  • BindToAllInterfaces确实像听起来一样.
  • BindToDefaultInterfaces (note plural) which will bind MyOtherBar to IMyOtherBar, IOtherBar & IBar (I think)
  • BindToSingleInterface works if every class implements exactly 1 interface.
  • BindToAllInterfaces does exactly what it sounds like.

我想更改为那些,但是我担心引入一些难以理解的错误,从而某些地方的类以应有的方式停止绑定,但是我没有注意到.

I'd like to change to those, but I'm concerned about introducing obscure bugs whereby some class somewhere stops binding in the way that it should, but I don't notice.

是否有任何方法可以对此进行测试/以合理的安全性进行此更改(无论如何,这不仅仅是做到并希望"!),而不仅仅是尝试找出如何锻炼每个可能的组件.

Is there any way to test this / make this change with a reasonable amount of safety (i.e. more than "do it and hope", anyway!) without just trying to work out how to excercise EVERY possible component.

推荐答案

因此,我设法解决了这个问题... 我的解决方案并非没有缺点,但从根本上实现了我想要的安全性.

So, I managed to solve this... My solution is not without its drawbacks, but it does fundamentally achieve the safety I wanted.


摘要

大致来说,有两个方面:

Roughly speaking there are 2 aspects:

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

两者的路径大致相同:

  • 重构您的DI配置代码,以便它的定义应用程序内容绑定的核心部分可以与其余启动代码隔离地运行.
  • 在测试开始时,请调用上面的DI配置代码,以使您拥有站点使用的内核对象的副本,可以测试其绑定
  • 进行一定程度的反射,以生成内核应能够提供的相关Type对象的列表.
  • (可选)对该列表进行过滤,以忽略一些您不需要测试的类和接口,例如,您的代码无需担心内核是否知道如何进行自我引导,因此它可以忽略它在属于您的DI框架的命名空间中的所有绑定.).
  • 然后循环遍历剩下的接口类型对象,并确保kernel.Get(interfaceType)运行时每个对象都没有异常.
  • Refactor your DI configuration code, so that the core portion of it that defines bindings for the meat of your app can be run in isolation from the rest of the Startup Code.
  • At the start of your Test invoke the above DI config code, so that you have a replica of the kernel object that your site uses, whose bindings you can test
  • perform some amount of Reflection, to generate a list of the relevant Type objects which the kernel should be able to provide.
  • (optional) filter that list to ignore some classes and interfaces that you know your tests don't need concern themselves about (e.g. your code doesn't need to worry about whether the Kernel knows how to bootstrap itself, so it can ignore any Bindings it has in the namespace belonging to your DI framework.).
  • Then loop over the Interface type objects you have left and ensure that kernel.Get(interfaceType) runs without an Exception for each one.

继续阅读以获取更多Gory详细信息...

验证所有已定义的内核绑定

这将是特定于所讨论的DI框架的,但是对于Ninject来说,它却是毛茸茸的...

This is going to be specific to the DI framework in question, but for Ninject it's pretty hairy...

如果Ninject内核具有内置的方式公开其绑定集合,那就更好了,但是事实并非如此.但是bindings集合是私有可用的,因此,如果您执行正确的Reflection咒语,就可以掌握它们.然后,您必须进行一些 more 反射,以将其Binding对象转换为{InterfaceType : ConcreteType}对.

It would be much nicer if a Ninject kernel had a built-in way to expose its collection of Bindings, but alas it doesn't. But the bindings collection is available privately, so if you perform the correct Reflection incantations you can get hold of them. You then have to do some more Reflection to convert its Binding objects into {InterfaceType : ConcreteType} pairs.

我将分别发布如何从Ninject中提取这些对象的细节,因为这与通常如何为DI config设置测试的问题正交. {#占位符以链接到该#}

I'll post the minutiae of how to extract these objects from Ninject separately, since that is orthogonal to the question of how to set up tests for DI config in general. {#Placeholder for a link to that#}

其他DI框架可以通过更公开地提供这些集合(甚至直接提供某种Validate()方法)来简化此过程.

Other DI Frameworks may make this easier by providing these collections more publicly (or even by providing some sort of Validate() method directly.)

一旦有了内核认为可以绑定的接口列表,就循环遍历并测试解决每个接口.

Once you have a list of the interface that the kernel thinks it can bind, just loop over them and test out resolving each one.

具体细节将随语言和测试框架而变化,但是我使用C#FluentAssertions,因此我分配了Action resolutionAction = (() => testKernel.Get(interfaceType))并声明了resolutionAction.ShouldNotThrow()或类似的内容.

Details of this will vary by Language and Testing Framework, but I use C# and FluentAssertions, so I assigned Action resolutionAction = (() => testKernel.Get(interfaceType)) and asserted resolutionAction.ShouldNotThrow() or something very similar.


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

上半部分都很好,但是它告诉您的是,您DI 拾取的绑定是定义明确的.它不会告诉您是否所有绑定都完全 missing .

The first half is all very well, but all it tells you is that the Bindings that you DI has picked up are well-defined. It doesn't tell you whether any Bindings are entirely missing.

您可以通过在代码库中收集所有有趣的程序集来解决这种情况:

You can cover that case by collecting all of the interesting Assemblies in your codebase:

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

然后为每个Assembly反映其类,以查找其公开的公共接口,并确保每个接口都可以被内核解析.

Then for each Assembly reflect over its classes to find the public Interfaces that it exposes, and ensure that each of those can be resolved by the kernel.

这种方法存在一些问题:

You've got a couple of issues with this approach:

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

这不是直接的问题,但这意味着您的测试无法像您想象的那样保护您.我进行了安全网测试,断言Ninject内核知道的每个程序集都应包含在要测试的程序集列表中.如果有人添加了一个新的程序集,则它很可能包含内核提供的 something ,因此此安全网测试将失败,从而使开发人员将注意力放在该测试类上.

This isn't directly a problem, but it would mean your tests don't protect you as well as you think. I put in a safety net test, to assert that every Assembly that the Ninject Kernel knows about should be in this list of Assemblies to be tested. If someone adds a new Assembly, it will likely contain something that is provided by the kernel, so this safety-net test will fail, bringing the developers attention to this test class.

  1. 内核没有提供的类呢?

我发现主要由于明显的原因未提供这些类-也许它们实际上是由Factory类提供的,或者该类使用不佳且是手动构造的.无论哪种方式,这些类都是少数,可以相对轻松地列为显式异常(遍历所有类;如果classname = foo,则忽略它.").

I found that mainly these classes were not provided for a clear reason - maybe they're actually provided by Factory classes, or maybe the class is badly used and is manually constructed. Either way these classes were a minority and could be listed as explicit exceptions ("loop over all classes; if classname = foo then ignore it.") relatively painlessly.


总体来说,这有点毛.而且更脆弱,我通常希望进行测试.


Overall, this is moderately hairy. And is more fragile that I'd generally like tests to be.

但是可以.

可能是您在进行更改之前编写的内容,仅仅是为了使您可以在更改之前运行一次,在更改之后运行一次,以检查是否有任何损坏,然后将其删除?

It might be something that you write before making the change, solely so that you can run it once before your change, once after the change to check that nothing's broken and then delete it?

这篇关于安全地对IoC/DI配置进行广泛的更改的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆