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.
I've been contributing to a small-ish codebase (maybe ~10 pages? and 20-30 services/controllers) which uses Ninject for Ioc/DI.
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.
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?
It all works so far, because everyone happens to have called their WhateverService interface IWhateverService
.
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 :)
There are a couple of alternative configurations:
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:
Both take roughly the same path:
Type
objects which the kernel should be able to provide.kernel.Get(interfaceType)
runs without an Exception for each one.Read on for more of the Gory details...
Validating all defined Kernel Bindings
This is going to be specific to the DI framework in question, but for Ninject
it's pretty hairy...
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.
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#}
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.
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.
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))
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:
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.
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.
But it works.
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?