Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow transitive dependencies #86

Merged
merged 6 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 53 additions & 0 deletions Moq.AutoMock.Tests/DescribeCreateInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,58 @@ public void It_throws_original_exception_caught_whilst_creating_object_with_orig
ArgumentException exception = Assert.ThrowsException<ArgumentException>(() => mocker.CreateInstance<ConstructorThrows>());
StringAssert.Contains(exception.StackTrace, typeof(ConstructorThrows).Name);
}

[TestMethod]
public void It_creates_object_when_first_level_dependencies_are_classes()
{
var mocker = new AutoMocker();
HasClassDependency instance = mocker.CreateInstance<HasClassDependency>();
var dependency = instance.WithService;
Assert.IsNotNull(dependency);
Assert.IsInstanceOfType(dependency, typeof(WithService));
Assert.IsInstanceOfType(Mock.Get(dependency), typeof(Mock<WithService>));
Assert.AreSame(dependency, mocker.Get<WithService>());
}

[TestMethod]
public void It_creates_object_with_2_first_level_dependencies()
{
var mocker = new AutoMocker();
var instance = mocker.CreateInstance<With2ClassDependencies>();

var dependency1 = instance.WithService;
Assert.IsNotNull(dependency1);
Assert.IsInstanceOfType(dependency1, typeof(WithService));
Assert.IsInstanceOfType(Mock.Get(dependency1), typeof(Mock<WithService>));
Assert.AreSame(dependency1, mocker.Get<WithService>());

var dependency2 = instance.With3Parameters;
Assert.IsNotNull(dependency2);
Assert.IsInstanceOfType(dependency2, typeof(With3Parameters));
Assert.IsInstanceOfType(Mock.Get(dependency2), typeof(Mock<With3Parameters>));
}

[TestMethod]
public void Second_level_dependencies_act_same_as_if_they_were_target()
{
var mocker = new AutoMocker();
var instance = mocker.CreateInstance<HasFuncDependencies>();
var dependency = instance.WithServiceFactory();
Assert.IsNotNull(dependency);
Assert.IsInstanceOfType(dependency, typeof(WithService));
Assert.IsInstanceOfType(Mock.Get(dependency), typeof(Mock<WithService>));
// Questionable if this is the correct behavior, but it is the current behavior.
Assert.AreSame(dependency, mocker.Get<WithService>());
}

[TestMethod]
public void It_creates_object_with_recursive_dependency()
adamhewitt627 marked this conversation as resolved.
Show resolved Hide resolved
{
var mocker = new AutoMocker();
// I could see this changing to something else in the future, like null. Right now, it seems
// best to cause early failure to clarify what went wrong. Also, returning null "allows" the
// behavior, so it's easier to move that direction later without breaking backward compatibility.
Assert.ThrowsException<InvalidOperationException>(mocker.CreateInstance<WithRecursiveDependency>);
}
}
}
12 changes: 12 additions & 0 deletions Moq.AutoMock.Tests/Util/HasClassDependency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Moq.AutoMock.Tests.Util
{
public class HasClassDependency
{
public WithService WithService { get; }

public HasClassDependency(WithService withService)
{
WithService = withService;
}
}
}
14 changes: 14 additions & 0 deletions Moq.AutoMock.Tests/Util/HasFuncDependencies.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace Moq.AutoMock.Tests.Util
{
public class HasFuncDependencies
{
public Func<WithService> WithServiceFactory { get; }

public HasFuncDependencies(Func<WithService> withServiceFactory)
{
WithServiceFactory = withServiceFactory;
}
}
}
16 changes: 16 additions & 0 deletions Moq.AutoMock.Tests/Util/With2ClassDependencies.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Moq.AutoMock.Tests.Util
{
#pragma warning disable CA1812 //is an internal class that is apparently never instantiated
internal class With2ClassDependencies
#pragma warning restore CA1812
{
public WithService WithService { get; }
public With3Parameters With3Parameters { get; }

public With2ClassDependencies(WithService withService, With3Parameters with3Parameters)
{
WithService = withService;
With3Parameters = with3Parameters;
}
}
}
2 changes: 1 addition & 1 deletion Moq.AutoMock.Tests/Util/With3Parameters.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Moq.AutoMock.Tests.Util
{
#pragma warning disable CA1801, CA1812 //is an internal class that is apparently never instantiated
internal class With3Parameters
public class With3Parameters
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a bit of a disjoint here. If this should be public then we should remove the CA1812 above and update the comment, or this could go back to internal

{
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedParameter.Local
Expand Down
12 changes: 12 additions & 0 deletions Moq.AutoMock.Tests/Util/WithRecursiveDependency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Moq.AutoMock.Tests.Util
{
public class WithRecursiveDependency
{
public WithRecursiveDependency Child { get; }

public WithRecursiveDependency(WithRecursiveDependency child)
{
Child = child;
}
}
}
54 changes: 30 additions & 24 deletions Moq.AutoMock/AutoMocker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public AutoMocker(MockBehavior mockBehavior, DefaultValue defaultValue, bool cal
/// </summary>
public ICollection<IMockResolver> Resolvers { get; }

private IInstance Resolve(Type serviceType)
private IInstance Resolve(Type serviceType, ResolutionContext resolutionContext)
{
if (serviceType.IsArray)
{
Expand All @@ -98,7 +98,7 @@ private IInstance Resolve(Type serviceType)
return instance;
}

object? resolved = Resolve(serviceType, null);
object? resolved = Resolve(serviceType, null, resolutionContext);
return resolved switch
{
Mock mock => new MockInstance(mock),
Expand All @@ -107,9 +107,16 @@ private IInstance Resolve(Type serviceType)
};
}

private object? Resolve(Type serviceType, object? initialValue)
private object? Resolve(Type serviceType, object? initialValue, ResolutionContext resolutionContext)
{
var context = new MockResolutionContext(this, serviceType, initialValue);
if (resolutionContext.VisitedTypes.Contains(serviceType))
{
throw new InvalidOperationException("Class could not be constructed because it appears to " +
$"be used recursively: '{serviceType}'");
}

resolutionContext.VisitedTypes.Add(serviceType);
var context = new MockResolutionContext(this, serviceType, initialValue, resolutionContext);

foreach (var r in Resolvers)
r.Resolve(context);
Expand Down Expand Up @@ -167,11 +174,11 @@ public object CreateInstance(Type type, bool enablePrivate)
{
if (type is null) throw new ArgumentNullException(nameof(type));

BindingFlags bindingFlags = GetBindingFlags(enablePrivate);
object?[] arguments = CreateArguments(type, bindingFlags);
var context = new ResolutionContext(enablePrivate);
object?[] arguments = CreateArguments(type, context);
try
{
var ctor = type.SelectCtor(_typeMap.Keys.ToArray(), bindingFlags);
var ctor = type.SelectCtor(_typeMap.Keys.ToArray(), context.BindingFlags);
return ctor.Invoke(arguments);
}
catch (TargetInvocationException e)
Expand Down Expand Up @@ -204,15 +211,16 @@ public object CreateInstance(Type type, bool enablePrivate)
/// <returns>An instance with virtual and abstract members mocked</returns>
public T CreateSelfMock<T>(bool enablePrivate) where T : class?
{
var arguments = CreateArguments(typeof(T), GetBindingFlags(enablePrivate));
var context = new ResolutionContext(enablePrivate);
var arguments = CreateArguments(typeof(T), context);

var mock = new Mock<T>(MockBehavior, arguments)
{
DefaultValue = DefaultValue,
CallBase = CallBase
};

var resolved = Resolve(typeof(T), mock);
var resolved = Resolve(typeof(T), mock, context);
if (resolved is Mock<T> m)
return m.Object;

Expand All @@ -229,7 +237,7 @@ public object CreateInstance(Type type, bool enablePrivate)
/// <typeparam name="TService">The type that the instance will be registered as</typeparam>
/// <param name="service"></param>
public void Use<TService>([DisallowNull] TService service)
=> Use(typeof(TService), service);
=> Use(typeof(TService), service ?? throw new ArgumentNullException(nameof(service)));

/// <summary>
/// Adds an instance to the container.
Expand Down Expand Up @@ -293,11 +301,16 @@ public void Use<TService>(Expression<Func<TService, bool>> setup)
/// <param name="serviceType">The type of service to retrieve</param>
/// <returns></returns>
public object Get(Type serviceType)
{
return Get(serviceType, new ResolutionContext(false));
}

private object Get(Type serviceType, ResolutionContext context)
{
if (serviceType is null) throw new ArgumentNullException(nameof(serviceType));

if (!_typeMap.TryGetValue(serviceType, out var instance) || instance is null)
instance = _typeMap[serviceType] = Resolve(serviceType);
instance = _typeMap[serviceType] = Resolve(serviceType, context);

if (instance is null)
throw new ArgumentException($"{serviceType} could not resolve to an object.", nameof(serviceType));
Expand Down Expand Up @@ -332,7 +345,7 @@ public Mock GetMock(Type serviceType)
private Mock GetMockImplementation(Type serviceType)
{
if (!_typeMap.TryGetValue(serviceType, out var instance) || instance is null)
instance = _typeMap[serviceType] = Resolve(serviceType);
instance = _typeMap[serviceType] = Resolve(serviceType, new ResolutionContext(false));

if (instance == null || !instance.IsMock)
throw new ArgumentException($"Registered service `{Get(serviceType)?.GetType()}` was not a mock");
Expand Down Expand Up @@ -418,7 +431,7 @@ public void Combine(Type type, params Type[] forwardTo)
{
if (type is null) throw new ArgumentNullException(nameof(type));

if (!(Resolve(type) is MockInstance mockObject))
if (!(Resolve(type, new ResolutionContext(false)) is MockInstance mockObject))
throw new ArgumentException($"{type} did not resolve to a Mock", nameof(type));

forwardTo.Aggregate(mockObject.Mock, As);
Expand Down Expand Up @@ -548,26 +561,19 @@ public void Verify()

#region Utilities

private static BindingFlags GetBindingFlags(bool enablePrivate)
{
var bindingFlags = BindingFlags.Instance | BindingFlags.Public;
if (enablePrivate) bindingFlags |= BindingFlags.NonPublic;
return bindingFlags;
}

private object?[] CreateArguments(Type type, BindingFlags bindingFlags)
internal object?[] CreateArguments(Type type, ResolutionContext context)
{
ConstructorInfo ctor = type.SelectCtor(_typeMap.Keys.ToArray(), bindingFlags);
ConstructorInfo ctor = type.SelectCtor(_typeMap.Keys.ToArray(), context.BindingFlags);
if (ctor is null)
throw new ArgumentException($"`{type}` does not have an acceptable constructor.", nameof(type));

return ctor.GetParameters().Select(x => Get(x.ParameterType)).ToArray();
return ctor.GetParameters().Select(x => Get(x.ParameterType, context)).ToArray();
}

private Mock GetOrMakeMockFor(Type type)
{
if (!_typeMap.TryGetValue(type, out var instance) || !instance.IsMock)
instance = Resolve(type);
instance = Resolve(type, new ResolutionContext(false));

if (!(instance is MockInstance mockInstance))
throw new ArgumentException($"{type} does not resolve to a Mock");
Expand Down
40 changes: 40 additions & 0 deletions Moq.AutoMock/ResolutionContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Moq.AutoMock
{
/// <summary>
/// Handles state while creating an object graph.
/// </summary>
public class ResolutionContext
adamhewitt627 marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Creates an instance with binding flags set according to `enablePrivate`.
/// </summary>
/// <param name="enablePrivate"></param>
public ResolutionContext(bool enablePrivate)
{
BindingFlags = GetBindingFlags(enablePrivate);
VisitedTypes = new HashSet<Type>();
}

/// <summary>
/// Flags passed to Mock constructor.
/// </summary>
public BindingFlags BindingFlags { get; }

/// <summary>
/// Used internally to track which types have been created inside a call graph,
/// to detect cycles in the object graph.
/// </summary>
public HashSet<Type> VisitedTypes { get; }
adamhewitt627 marked this conversation as resolved.
Show resolved Hide resolved

private static BindingFlags GetBindingFlags(bool enablePrivate)
{
var bindingFlags = BindingFlags.Instance | BindingFlags.Public;
if (enablePrivate) bindingFlags |= BindingFlags.NonPublic;
return bindingFlags;
}
}
}
21 changes: 21 additions & 0 deletions Moq.AutoMock/Resolvers/Array.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Moq.AutoMock.Resolvers
{
#if NET45
//Array.Empty does not exist in net45
//This duplicates what was added to .NET Core
internal static class Array
{
public static T[] Empty<T>()
{
return EmptyArray<T>.Value;
}

private static class EmptyArray<T>
{
#pragma warning disable CA1825 // this is the implementation of Array.Empty<T>()
internal static readonly T[] Value = new T[0];
#pragma warning restore CA1825
}
}
#endif
}
13 changes: 12 additions & 1 deletion Moq.AutoMock/Resolvers/MockResolutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ public class MockResolutionContext
/// <param name="autoMocker">The <c>AutoMocker</c> instance.</param>
/// <param name="requestType">The requested type to resolve.</param>
/// <param name="initialValue">The initial value to use.</param>
public MockResolutionContext(AutoMocker autoMocker, Type requestType, object? initialValue)
/// <param name="objectGraphContext">
/// Context within the object graph being created. This differs from the MockResolutionContext which is
/// only relevant for a single object creation.
/// </param>
public MockResolutionContext(AutoMocker autoMocker, Type requestType, object? initialValue, ResolutionContext objectGraphContext)
{
AutoMocker = autoMocker ?? throw new ArgumentNullException(nameof(autoMocker));
RequestType = requestType ?? throw new ArgumentNullException(nameof(requestType));
Value = initialValue;
ObjectGraphContext = objectGraphContext;
adamhewitt627 marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
Expand All @@ -34,6 +39,12 @@ public MockResolutionContext(AutoMocker autoMocker, Type requestType, object? in
/// The value to use from the resolution.
/// </summary>
public object? Value { get; set; }

/// <summary>
/// Context within the object graph being created. This differs from the MockResolutionContext which is
/// only relevant for a single object creation.
/// </summary>
public ResolutionContext ObjectGraphContext { get; }

/// <summary>
/// Deconstruct this instance into its individual properties.
Expand Down