Update ServiceProviderBuidlerBase to allow for that handling the TestSettingsInjectorBase. Need to make the ServiceProviderBuilderBase.WithService Virtual. May need to update the ServiceProviderBuilderBase.WithService() overloads to call the WithService(T service) as well to ensure that it always gets called.
/// <summary>
/// Abstract base class for building a service provider that also sets the services if the code to be tested utilizes the IIocServiceProviderBuilder. This allows for defining in a single location in the test project all service implementations.
/// </summary>
/// <typeparam name="TDerived">The type of the derived class.</typeparam>
/// <typeparam name="TInjector">The type of the injector settings.</typeparam>
public abstract class GenericServiceProviderBuilder<TDerived, TInjector> : DLaB.Xrm.Test.Builders.ServiceProviderBuilderBase<TDerived>
where TDerived : GenericServiceProviderBuilder<TDerived, TInjector>
where TInjector : TestSettingsInjectorBase
{
private readonly TInjector _testSettingInjector;
/// <summary>
/// Initializes a new instance of the <see cref="GenericServiceProviderBuilder{TDerived, TInjector}"/> class.
/// </summary>
/// <param name="service">The organization service.</param>
/// <param name="context">The plugin execution context.</param>
/// <param name="trace">The tracing service.</param>
/// <param name="injector">The test settings injector.</param>
protected GenericServiceProviderBuilder(IOrganizationService service, IPluginExecutionContext context, ITracingService trace, TInjector injector = null) : base(service, context, trace)
{
_testSettingInjector = injector ?? Activator.CreateInstance<TInjector>();
base.WithService<IIocServiceProviderBuilder>(_testSettingInjector);
// Registers the tracing service, so it still will get set if the code to be tested utilizes the IIocServiceProviderBuilder.
_testSettingInjector.Registrations.Add(c => c.AddSingleton(service));
_testSettingInjector.Registrations.Add(c => c.AddSingleton(context));
_testSettingInjector.Registrations.Add(c => c.AddSingleton(trace));
}
/// <summary>
/// Adds custom registrations to the IoC container after the registrations of the Test Defaults, allowing for each test method to override the test defaults when the code to be tested utilizes the IIocServiceProviderBuilder
/// </summary>
/// <param name="register">The action to register services.</param>
/// <returns>The derived builder instance.</returns>
public TDerived WithRegistrations(Action<IIocContainer> register)
{
_testSettingInjector.Registrations.Add(register);
return This;
}
/// <summary>
/// Adds a service to the service provider, as well as registering it to ensure that if the code to be tested utilizes the IIocServiceProviderBuilder, it will still be set.
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
/// <param name="service">The service instance.</param>
/// <returns>The derived builder instance.</returns>
public new TDerived WithService<T>(T service)
{
base.WithService(service);
WithRegistrations(c => c.AddSingleton(service));
return This;
}
}
/// <summary>
/// Abstract base class for injecting test settings into the IoC container.
/// </summary>
public abstract class TestSettingsInjectorBase : IIocServiceProviderBuilder
{
/// <summary>
/// Gets the list of registration actions.
/// </summary>
public List<Action<IIocContainer>> Registrations { get; } = new List<Action<IIocContainer>>();
/// <summary>
/// Gets the list of built service providers.
/// </summary>
public List<IServiceProvider> BuiltServiceProviders { get; } = new List<IServiceProvider>();
/// <summary>
/// Builds the service provider with the specified container and provider.
/// </summary>
/// <param name="provider">The service provider.</param>
/// <param name="container">The IoC container.</param>
/// <returns>The built service provider.</returns>
public IServiceProvider BuildServiceProvider(IServiceProvider provider, IIocContainer container)
{
RegisterTestDefaults(container);
foreach (var registration in Registrations)
{
registration(container);
}
var newProvider = container.BuildServiceProvider(provider);
BuiltServiceProviders.Add(newProvider);
return newProvider;
}
/// <summary>
/// Registers the default test settings in the IoC container.
/// </summary>
/// <param name="container">The IoC container.</param>
/// <returns>The IoC container with registered defaults.</returns>
public abstract IIocContainer RegisterTestDefaults(IIocContainer container);
}
How this really works:
In Test Method:
var serviceProvider = new ServiceProviderBuilder(service, context, Logger).Build();
plugin.Execute(serviceProvider);
Logger is an ITestLogger, so the ServiceProviderBuilder will wrap it an an FakeTracingService and then get's registered to the service provider in the builder as an ITracingService. All good so far, but if the ServiceProviderBuilder directly inherits DLaB.Xrm.Test.Builders.ServiceProviderBuilderBase and not GenericServiceProviderBuilder<TDerived, TInjector> and the plugin code utilizes the IIocServiceProviderBuilder, which the DLaBGenericPluginBase class does:
/// <summary>
/// Allows for Injecting the Service Provider for the Plugin Execution via the IIoCServiceProviderBuilder. Sets the created injected service via the IIoCServiceProviderBuilder
/// </summary>
/// <param name="serviceProvider">The Service Provider from Dataverse.</param>
/// <returns></returns>
protected virtual IServiceProvider InjectServiceProvider(IServiceProvider serviceProvider)
{
// The dataverse serviceProvider will not have an IIoCServiceProviderBuilder, so the default Container.BuildServiceProvider will be used, but tests can inject their own builder that overrides registrations.
var serviceProviderBuilder = serviceProvider.Get<IIocServiceProviderBuilder>();
if (serviceProviderBuilder == null)
{
return Container.BuildServiceProvider(serviceProvider);
}
var injectedProvider = serviceProviderBuilder.BuildServiceProvider(serviceProvider, Container);
serviceProviderBuilder.BuiltServiceProviders.Add(injectedProvider);
return injectedProvider;
}
then the actual service provider used by the plugin (aka "the code to be tested") will be the one created from the IIocServiceProviderBuilder.BuilderSerivceProvider and if that service provider provides a default implementation for ITracingService, then the one defined in the unit test is ignored since it is overwritten by the provided default implementation. And assuming the plugin does at least a single trace, this test method will fail:
var serviceProvider = new ServiceProviderBuilder(service, context, Logger).Build();
plugin.Execute(serviceProvider);
Assert.IsTrue(serviceProvider.GetFake<FakeTraceService>().Traces.Any());
as well as this one:
var tracer = new FakeTraceService(Logger);
var serviceProvider = new ServiceProviderBuilder(service, context, tracer).Build();
plugin.Execute(serviceProvider);
Assert.IsTrue(tracer.Traces.Any());
There are two things that should be done to resolve this issue:
- Retrieve the
IIocServiceProviderBuilder and access the created service provider via the BuiltServiceProviders. This resolves the first issue listed above because now the actual FakeTraceService is being retrieved from the ServiceProvider used by the code being tested:
var serviceProvider = new ServiceProviderBuilder(service, context, Logger).Build();
plugin.Execute(serviceProvider);
Assert.IsTrue(serviceProvider.GetRequiredService<IIocServiceProviderBuilder>().BuiltServiceProviders.Last().GetFake<FakeTraceService>().Traces.Any());
- Have
ServiceProviderBuilder inherit from GenericServiceProviderBuilder<ServiceProviderBuilder, TestSettingsInjector>. In this way, the tracer that is passed, actually is the one that will be returned by the ServiceProvider created by the IIocServiceProvideBuilder
Update ServiceProviderBuidlerBase to allow for that handling the TestSettingsInjectorBase. Need to make the ServiceProviderBuilderBase.WithService Virtual. May need to update the ServiceProviderBuilderBase.WithService() overloads to call the WithService(T service) as well to ensure that it always gets called.
How this really works:
In Test Method:
Loggeris anITestLogger, so theServiceProviderBuilderwill wrap it an anFakeTracingServiceand then get's registered to the service provider in the builder as anITracingService. All good so far, but if theServiceProviderBuilderdirectly inheritsDLaB.Xrm.Test.Builders.ServiceProviderBuilderBaseand notGenericServiceProviderBuilder<TDerived, TInjector>and the plugin code utilizes theIIocServiceProviderBuilder, which the DLaBGenericPluginBase class does:then the actual service provider used by the plugin (aka "the code to be tested") will be the one created from the
IIocServiceProviderBuilder.BuilderSerivceProviderand if that service provider provides a default implementation forITracingService, then the one defined in the unit test is ignored since it is overwritten by the provided default implementation. And assuming the plugin does at least a single trace, this test method will fail:as well as this one:
There are two things that should be done to resolve this issue:
IIocServiceProviderBuilderand access the created service provider via theBuiltServiceProviders. This resolves the first issue listed above because now the actualFakeTraceServiceis being retrieved from theServiceProviderused by the code being tested:ServiceProviderBuilderinherit fromGenericServiceProviderBuilder<ServiceProviderBuilder, TestSettingsInjector>. In this way, the tracer that is passed, actually is the one that will be returned by theServiceProvidercreated by theIIocServiceProvideBuilder