Skip to content

Commit 5d2f7b4

Browse files
authored
Enable customization of the registration sources via a configurable options object (#42)
This change enables customization of the registration sources via a configurable options object. There are two options exposed: 1. MockHandlers - these allow injecting code to handle a mock upon creation 2. ConfigureAnyConcreteTypeRegistration - this allows modifying the options AnyConcreteTypeNotAlreadyRegisteredSource uses As part of this, a couple of convenience methods are provided: 1. AutoSubstituteBuilder.InjectPropertiesToMocks() - This enables auto injection of properties on the NSubstitute mock from the configured container 2. AutoSubstituteBuilder.MakeUnregisteredTypesPerLifetime() - This configures the AnyConcreteTypeNotAlreadyRegisteredSource to be per lifetime scope
1 parent 962e1bb commit 5d2f7b4

File tree

7 files changed

+347
-18
lines changed

7 files changed

+347
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using Autofac;
2+
using Autofac.Core;
3+
using NUnit.Framework;
4+
5+
namespace AutofacContrib.NSubstitute.Tests
6+
{
7+
[TestFixture]
8+
public class AutoSubstituteOptionsFixture
9+
{
10+
[Test]
11+
public void NoInjectPropertiesInterface()
12+
{
13+
using var mock = AutoSubstitute.Configure()
14+
.Build()
15+
.Container;
16+
17+
var testInterface1 = mock.Resolve<ITestInterface1>();
18+
var class1 = mock.Resolve<Class1>();
19+
20+
Assert.AreNotSame(class1, testInterface1.Instance);
21+
}
22+
23+
[Test]
24+
public void InjectPropertiesInterface()
25+
{
26+
using var mock = AutoSubstitute.Configure()
27+
.InjectProperties()
28+
.Build()
29+
.Container;
30+
31+
var testInterface1 = mock.Resolve<ITestInterface1>();
32+
33+
Assert.That(testInterface1.Instance, Is.TypeOf<Class1>());
34+
}
35+
36+
[Test]
37+
public void InjectPropertiesInterfaceWithUnregisteredPerLifetime()
38+
{
39+
using var mock = AutoSubstitute.Configure()
40+
.InjectProperties()
41+
.MakeUnregisteredTypesPerLifetime()
42+
.Build()
43+
.Container;
44+
45+
var class1 = mock.Resolve<Class1>();
46+
var testInterface1 = mock.Resolve<ITestInterface1>();
47+
48+
Assert.AreSame(class1, testInterface1.Instance);
49+
}
50+
51+
[Test]
52+
public void NoInjectPropertiesClass()
53+
{
54+
using var mock = AutoSubstitute.Configure()
55+
.Build()
56+
.Container;
57+
58+
var obj = mock.Resolve<WithProperties>();
59+
60+
Assert.IsNull(obj.Service);
61+
}
62+
63+
[Test]
64+
public void InjectPropertiesClass()
65+
{
66+
using var mock = AutoSubstitute.Configure()
67+
.InjectProperties()
68+
.Build()
69+
.Container;
70+
71+
var obj = mock.Resolve<WithProperties>();
72+
73+
Assert.IsNotNull(obj.Service);
74+
}
75+
76+
[Test]
77+
public void InjectPropertiesInterfaceRecursive()
78+
{
79+
using var mock = AutoSubstitute.Configure()
80+
.InjectProperties()
81+
.MakeUnregisteredTypesPerLifetime()
82+
.Build()
83+
.Container;
84+
85+
var obj = mock.Resolve<ITestInterfaceRecursive>();
86+
87+
Assert.AreSame(obj, obj.Instance.Recursive);
88+
}
89+
90+
[Test]
91+
public void InternalConstructorFails()
92+
{
93+
using var mock = AutoSubstitute.Configure()
94+
.Build()
95+
.Container;
96+
97+
Assert.Throws<DependencyResolutionException>(() => mock.Resolve<ClassWithInternalConstructor>());
98+
}
99+
100+
[Test]
101+
public void InternalConstructorSucceeds()
102+
{
103+
using var mock = AutoSubstitute.Configure()
104+
.UnregisteredTypesUseInternalConstructor()
105+
.Build()
106+
.Container;
107+
108+
Assert.NotNull(mock.Resolve<ClassWithInternalConstructor>());
109+
}
110+
111+
public interface ITestInterface1
112+
{
113+
Class1 Instance { get; }
114+
}
115+
116+
public class WithProperties
117+
{
118+
public ITestInterface1 Service { get; set; }
119+
}
120+
121+
public class Class1
122+
{
123+
}
124+
125+
public interface ITestInterfaceRecursive
126+
{
127+
ClassRecursive Instance { get; }
128+
}
129+
130+
public class ClassRecursive
131+
{
132+
public ITestInterfaceRecursive Recursive { get; set; }
133+
}
134+
135+
public class ClassWithInternalConstructor
136+
{
137+
internal ClassWithInternalConstructor()
138+
{
139+
}
140+
}
141+
}
142+
}

AutofacContrib.NSubstitute/AutoSubstituteBuilder.cs

+24-2
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,31 @@ public class AutoSubstituteBuilder
1212
private readonly Dictionary<Type, object> _substituteForRegistrations = new Dictionary<Type, object>();
1313
private readonly List<IProvidedValue> _providedValues;
1414
private readonly ContainerBuilder _builder;
15+
private readonly AutoSubstituteOptions _options;
1516

1617
public AutoSubstituteBuilder()
1718
{
1819
_builder = new ContainerBuilder();
1920
_providedValues = new List<IProvidedValue>();
21+
_options = new AutoSubstituteOptions();
2022
}
2123

2224
public AutoSubstitute Build()
2325
=> new AutoSubstitute(InternalBuild());
2426

2527
internal IContainer InternalBuild()
2628
{
27-
_builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());
28-
_builder.RegisterSource(new NSubstituteRegistrationHandler());
29+
_builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource
30+
{
31+
RegistrationConfiguration = c =>
32+
{
33+
foreach (var configure in _options.ConfigureAnyConcreteTypeRegistration)
34+
{
35+
configure(c);
36+
}
37+
}
38+
});
39+
_builder.RegisterSource(new NSubstituteRegistrationHandler(_options));
2940

3041
var container = _builder.Build();
3142

@@ -37,6 +48,17 @@ internal IContainer InternalBuild()
3748
return container;
3849
}
3950

51+
/// <summary>
52+
/// Configures the option associated with this substitue builder.
53+
/// </summary>
54+
/// <param name="action">A delegate that configures the <see cref="AutoSubstituteOptions"/>.</param>
55+
/// <returns>The <see cref="AutoSubstituteBuilder"/>.</returns>
56+
public AutoSubstituteBuilder ConfigureOptions(Action<AutoSubstituteOptions> action)
57+
{
58+
action(_options);
59+
return this;
60+
}
61+
4062
/// <summary>
4163
/// Provides direct access to the <see cref="ContainerBuilder"/> to manipulate how needed.
4264
/// </summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Autofac.Builder;
2+
using System;
3+
using System.Collections.Generic;
4+
5+
namespace AutofacContrib.NSubstitute
6+
{
7+
public class AutoSubstituteOptions
8+
{
9+
/// <summary>
10+
/// Gets a collection of handlers that can be used to modify mocks after they are created.
11+
/// </summary>
12+
public ICollection<MockHandler> MockHandlers { get; } = new List<MockHandler>();
13+
14+
/// <summary>
15+
/// Gets a collection of delegates that can augment the registrations of objects created but not registered.
16+
/// </summary>
17+
public ICollection<Action<IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle>>> ConfigureAnyConcreteTypeRegistration { get; } = new List<Action<IRegistrationBuilder<object, ConcreteReflectionActivatorData, SingleRegistrationStyle>>>();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Autofac;
2+
using Autofac.Core.Activators.Reflection;
3+
using NSubstitute.Core;
4+
using System;
5+
using System.Reflection;
6+
7+
namespace AutofacContrib.NSubstitute
8+
{
9+
public static class AutoSubstituteOptionsExtensions
10+
{
11+
/// <summary>
12+
/// Configures the auto-generated types Autofac creates to be the same throughout a lifetime scope.
13+
/// </summary>
14+
/// <param name="builder">A <see cref="AutoSubstituteBuilder"/></param>
15+
/// <returns>The supplied <see cref="AutoSubstituteBuilder"/></returns>
16+
public static AutoSubstituteBuilder MakeUnregisteredTypesPerLifetime(this AutoSubstituteBuilder builder)
17+
=> builder.ConfigureOptions(options =>
18+
{
19+
options.ConfigureAnyConcreteTypeRegistration.Add(registration => registration.InstancePerLifetimeScope());
20+
});
21+
22+
/// <summary>
23+
/// Configures the auto-generated types Autofac creates to search through internal constructors as well. If a type is explicitly registered, it will not search for internal constructors automatically.
24+
/// </summary>
25+
/// <param name="builder">A <see cref="AutoSubstituteBuilder"/></param>
26+
/// <returns>The supplied <see cref="AutoSubstituteBuilder"/></returns>
27+
public static AutoSubstituteBuilder UnregisteredTypesUseInternalConstructor(this AutoSubstituteBuilder builder)
28+
=> builder.ConfigureOptions(options =>
29+
{
30+
options.ConfigureAnyConcreteTypeRegistration.Add(registration => registration.FindConstructorsWith(NonPublicConstructorFinder.Finder));
31+
});
32+
33+
/// <summary>
34+
/// Configures the auto-generated types to populate the properties based on the configured container. If a type is explicitly registered, then the properties will not be automatically satisfied.
35+
/// </summary>
36+
/// <param name="builder">A <see cref="AutoSubstituteBuilder"/></param>
37+
/// <returns>The supplied <see cref="AutoSubstituteBuilder"/></returns>
38+
public static AutoSubstituteBuilder InjectProperties(this AutoSubstituteBuilder builder)
39+
=> builder.ConfigureOptions(options =>
40+
{
41+
options.MockHandlers.Add(new AutoPropertyInjectorMockHandler());
42+
options.ConfigureAnyConcreteTypeRegistration.Add(r => r.PropertiesAutowired());
43+
});
44+
45+
private class NonPublicConstructorFinder : DefaultConstructorFinder
46+
{
47+
public static IConstructorFinder Finder { get; } = new NonPublicConstructorFinder();
48+
49+
private NonPublicConstructorFinder()
50+
: base(type => type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
51+
{
52+
}
53+
}
54+
55+
private class AutoPropertyInjectorMockHandler : MockHandler
56+
{
57+
public override void OnMockCreated(object instance, Type type, IComponentContext context, ISubstitutionContext substitutionContext)
58+
{
59+
var router = substitutionContext.GetCallRouterFor(instance);
60+
61+
router.RegisterCustomCallHandlerFactory(_ => new AutoPropertyInjectorCallHandler(context));
62+
}
63+
64+
private class AutoPropertyInjectorCallHandler : ICallHandler
65+
{
66+
private readonly IComponentContext _context;
67+
68+
public AutoPropertyInjectorCallHandler(IComponentContext context)
69+
{
70+
_context = context;
71+
}
72+
73+
public RouteAction Handle(ICall call)
74+
{
75+
var property = call.GetMethodInfo().GetPropertyFromGetterCallOrNull();
76+
77+
if (property is null)
78+
{
79+
return RouteAction.Continue();
80+
}
81+
82+
return RouteAction.Return(_context.Resolve(call.GetReturnType()));
83+
}
84+
}
85+
}
86+
}
87+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Autofac;
2+
using NSubstitute.Core;
3+
using System;
4+
5+
namespace AutofacContrib.NSubstitute
6+
{
7+
/// <summary>
8+
/// A handler that is used to modify any of the auto-generated NSubstitute mocks.
9+
/// </summary>
10+
public abstract class MockHandler
11+
{
12+
protected MockHandler()
13+
{
14+
}
15+
16+
/// <summary>
17+
/// Provides a way to manage mocks after creation but before returned from the container registry.
18+
/// </summary>
19+
/// <param name="instance">The mock instance.</param>
20+
/// <param name="type">The type the mock was created for.</param>
21+
/// <param name="context">The current component context.</param>
22+
/// <param name="substitutionContext">The current substitution context.</param>
23+
public abstract void OnMockCreated(object instance, Type type, IComponentContext context, ISubstitutionContext substitutionContext);
24+
}
25+
}

0 commit comments

Comments
 (0)