I want to do some integration testing using Simple Injector. However, sometimes I need to check whether a specific internal service has been called with the correct arguments. I am already using FakeItEasy in the test project, so my approach was to do this:
container.Options.RegisterResolveInterceptor(
(context, producer) =>
{
// this is the instance as it was provided by the container
object instance = producer();
object spy = FakeItEasy.Sdk.Create.Fake(context.Producer.ServiceType, options => options.Wrapping(instance));
Register(context.Producer.ServiceType, spy);
return spy;
});
While Register(...)
keeps track of spies so that I can look the spy up after the invocation and do the required check later.
But I learnt, this only intercepts the directly resolved instances, not the implicitly created dependent instances. So I checked out the Interception Extensions of Simple Injector. But these snippets still rely on RealProxy
and I didn't manage to make it .NET Core compliant using DispatchProxy
, probably just because of my lack of deep experience with expression tree manipulation.
Then I found this answer and the ApplyInterceptor
snippet, but while this approach seems promising, I am struggling to adapt the snippet so that the interceptor not just gets the factory method, but also gets to know the resolved service type:
container.Options.ApplyInterceptor(
factory =>
{
object instance = factory();
object spy = FakeItEasy.Sdk.Create.Fake(howToGetTheServiceType, options => options.Wrapping(instance));
Register(howToGetTheServiceType, spy);
return instance;
});
Any other suggestions?
The ApplyInterceptor
extension method from the documentation makes use of the ExpressionBuilding
event. This event is unsuited for interception. Although it allows you to replace the constructed instance with another and even change types, the post condition is that the returned type must be the same implementation type of a sub type of the implementation.
For instance, when a Register<ILogger, ConsoleLogger>()
registration is made, an ExpressionBuilding
handler may replace the expression for something that returns ConsoleLogger
or ConsoleLoggerSubClass : ConsoleLogger
, but never for a FileLogger : ILogger
.
For your quest, you need to use the ExpressionBuilt
event instead, as this is allowed to replace ConsoleLogger
with a FileLogger
.
The following snippet, hopefully, gets you started:
container.ExpressionBuilt += (s, e) =>
{
// Compile the original object creation into a delegate.
var factory =
(Func<object>)Expression.Lambda(typeof(Func<object>), e.Expression).Compile();
// Create a registration for the spy, based on the original lifestyle
var spyregistration = e.Lifestyle.CreateRegistration(
e.RegisteredServiceType,
() =>
{
var instance = factory();
var spy = FakeItEasy.Sdk.Create
.Fake(e.RegisteredServiceType, options => options.Wrapping(instance));
RegisterSpy(e.RegisteredServiceType, spy);
return spy;
},
container);
// Replace expression of the registration with the spy registration.
e.Expression = spyregistration.BuildExpression();
};