diff --git a/Moq.AutoMock.Tests/DescribeCreatingSelfMocks.cs b/Moq.AutoMock.Tests/DescribeCreatingSelfMocks.cs index 17940f55..90c781b6 100644 --- a/Moq.AutoMock.Tests/DescribeCreatingSelfMocks.cs +++ b/Moq.AutoMock.Tests/DescribeCreatingSelfMocks.cs @@ -1,24 +1,24 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq.AutoMock.Tests.Util; - -namespace Moq.AutoMock.Tests; - -[TestClass] -public class DescribeCreatingSelfMocks -{ - [TestMethod] - public void Self_mocks_are_useful_for_testing_most_of_class() - { - var mocker = new AutoMocker(); - var selfMock = mocker.CreateSelfMock(); - selfMock.TellJoke(); - Assert.IsFalse(selfMock.SelfDepricated); +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq.AutoMock.Tests.Util; + +namespace Moq.AutoMock.Tests; + +[TestClass] +public class DescribeCreatingSelfMocks +{ + [TestMethod] + public void Self_mocks_are_useful_for_testing_most_of_class() + { + AutoMocker mocker = new(); + var selfMock = mocker.CreateSelfMock(); + selfMock.TellJoke(); + Assert.IsFalse(selfMock.SelfDepricated); } [TestMethod] public void It_can_self_mock_objects_with_constructor_arguments() { - var mocker = new AutoMocker(); + AutoMocker mocker = new(); var selfMock = mocker.CreateSelfMock(); Assert.IsNotNull(selfMock.Service); Assert.IsNotNull(Mock.Get(selfMock.Service)); @@ -28,11 +28,60 @@ public void It_can_self_mock_objects_with_constructor_arguments() [Description("Issue 130")] public void It_reuses_dependencies_when_creating_self_mock() { - var mocker = new AutoMocker(); + AutoMocker mocker = new(); var service = mocker.CreateSelfMock(); Assert.IsTrue(ReferenceEquals(service.Dependency, mocker.GetMock().Object)); } + [TestMethod] + [Description("Issue 134")] + public void It_can_specify_self_mock_with_different_defaults() + { + AutoMocker mocker = new(MockBehavior.Loose); + var fooService = mocker.CreateSelfMock(mockBehavior: MockBehavior.Strict, defaultValue: DefaultValue.Mock, callBase: true); + + var mock = Mock.Get(fooService); + Assert.AreEqual(MockBehavior.Strict, mock.Behavior); + Assert.AreEqual(DefaultValue.Mock, mock.DefaultValue); + Assert.AreEqual(true, mock.CallBase); + } + + [TestMethod] + [Description("Issue 134")] + public void It_can_perform_setup_on_interface_of_self_mock_that_is_registered() + { + AutoMocker mocker = new(); + var service = mocker.WithSelfMock(); + mocker.Setup(s => s.Foo()).Returns(24); + } + + [TestMethod] + [Description("Issue 134")] + public void It_can_perform_setup_class_of_self_mock_that_is_registered() + { + AutoMocker mocker = new(); + var service = mocker.WithSelfMock(); + mocker.Setup(s => s.Foo()).Returns(24); + } + + [TestMethod] + [Description("Issue 134")] + public void It_can_perform_setup_on_interface_of_self_mock_that_is_registered_without_generics() + { + AutoMocker mocker = new(); + var service = mocker.WithSelfMock(typeof(IFooService), typeof(FooService)); + mocker.Setup(s => s.Foo()).Returns(24); + } + + [TestMethod] + [Description("Issue 134")] + public void It_can_perform_setup_class_of_self_mock_that_is_registered_without_generics() + { + AutoMocker mocker = new(); + var service = mocker.WithSelfMock(typeof(FooService)); + mocker.Setup(s => s.Foo()).Returns(24); + } + public abstract class AbstractService { public IDependency Dependency { get; } @@ -40,4 +89,13 @@ public abstract class AbstractService } public interface IDependency { } -} + + public interface IFooService + { + int Foo(); + } + public class FooService : IFooService + { + public virtual int Foo() => 42; + } +} diff --git a/Moq.AutoMock/AutoMocker.cs b/Moq.AutoMock/AutoMocker.cs index d10f5d96..9025e874 100644 --- a/Moq.AutoMock/AutoMocker.cs +++ b/Moq.AutoMock/AutoMocker.cs @@ -152,7 +152,7 @@ private bool TryResolve(Type serviceType, return true; } - #region Create Instance/SelfMock + #region Create Instance /// /// Constructs an instance from known services. Any dependencies (constructor arguments) @@ -222,6 +222,10 @@ public object CreateInstance(Type type, bool enablePrivate) } } + #endregion Create Instance + + #region CreateSelfMock + /// /// Constructs a self-mock from the services available in the container. A self-mock is /// a concrete object that has virtual and abstract members mocked. The purpose is so that @@ -233,7 +237,7 @@ public object CreateInstance(Type type, bool enablePrivate) public T CreateSelfMock() where T : class? => CreateSelfMock(false); - /// + /// /// Constructs a self-mock from the services available in the container. A self-mock is /// a concrete object that has virtual and abstract members mocked. The purpose is so that /// you can test the majority of a class but mock out a resource. This is great for testing @@ -243,26 +247,168 @@ public T CreateSelfMock() where T : class? /// When true, non-public constructors will also be used to create mocks. /// An instance with virtual and abstract members mocked public T CreateSelfMock(bool enablePrivate) where T : class? + => CreateSelfMock(enablePrivate, MockBehavior, DefaultValue, CallBase); + + /// + /// Constructs a self-mock from the services available in the container. A self-mock is + /// a concrete object that has virtual and abstract members mocked. The purpose is so that + /// you can test the majority of a class but mock out a resource. This is great for testing + /// abstract classes, or avoiding breaking cohesion even further with a non-abstract class. + /// + /// The instance that you want to build + /// When true, non-public constructors will also be used to create mocks. + /// Sets the Behavior property on the created Mock. + /// Sets the DefaultValue propert on the created Mock. + /// Sets the CallBase property on the created Mock. + /// An instance with virtual and abstract members mocked + public T CreateSelfMock( + bool enablePrivate = false, + MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + bool? callBase = null) + where T : class? { - var context = new ObjectGraphContext(enablePrivate); - if (!TryGetConstructorInvocation(typeof(T), context, out ConstructorInfo? ctor, out IInstance[]? arguments)) + return BuildSelfMock(enablePrivate, mockBehavior ?? MockBehavior, defaultValue ?? DefaultValue, callBase ?? CallBase).Object; + } + + /// + /// This constructs a self mock similar to . + /// The created mock instance is automatically registered using both its implementation and service type. + /// + /// The service type + /// The implementation type + /// When true, non-public constructors will also be used to create mocks. + /// Sets the Behavior property on the created Mock. + /// Sets the DefaultValue propert on the created Mock. + /// Sets the CallBase property on the created Mock. + /// An instance with virtual and abstract members mocked + public TImplementation WithSelfMock( + bool enablePrivate = false, + MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + bool? callBase = null) + where TImplementation : class, TService + where TService : class + { + Mock selfMock = BuildSelfMock(enablePrivate, + mockBehavior ?? MockBehavior, + defaultValue ?? DefaultValue, + callBase ?? CallBase); + WithTypeMap(typeMap => { - throw new ArgumentException( - $"Did not find a best constructor for `{typeof(T)}`. If your type has a non-public constructor, set the 'enablePrivate' parameter to true for this {nameof(AutoMocker)} method."); - } + typeMap[typeof(TImplementation)] = new MockInstance(selfMock); + typeMap[typeof(TService)] = new MockInstance(selfMock.As()); + }); + return selfMock.Object; + } - CacheInstances(arguments.Zip(ctor.GetParameters(), (i, p) => (p.ParameterType, i))); + /// + /// This constructs a self mock similar to . + /// The created mock instance is automatically registered using both its implementation and service type. + /// + /// The service type + /// When true, non-public constructors will also be used to create mocks. + /// Sets the Behavior property on the created Mock. + /// Sets the DefaultValue propert on the created Mock. + /// Sets the CallBase property on the created Mock. + /// An instance with virtual and abstract members mocked + public T WithSelfMock( + bool enablePrivate = false, + MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + bool? callBase = null) + where T : class + { + Mock selfMock = BuildSelfMock(enablePrivate, mockBehavior ?? MockBehavior, defaultValue ?? DefaultValue, callBase ?? CallBase); + WithTypeMap(typeMap => + { + typeMap[typeof(T)] = new MockInstance(selfMock); + }); + return selfMock.Object; + } - var mock = new Mock(MockBehavior, arguments.Select(x => x.Value).ToArray()) + /// + /// This constructs a self mock similar to . + /// The created mock instance is automatically registered using both its implementation and service type. + /// + /// The service type. + /// The implementation type of the service. + /// When true, non-public constructors will also be used to create mocks. + /// Sets the Behavior property on the created Mock. + /// Sets the DefaultValue propert on the created Mock. + /// Sets the CallBase property on the created Mock. + /// An instance with virtual and abstract members mocked + public object WithSelfMock( + Type serviceType, + Type implementationType, + bool enablePrivate = false, + MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + bool? callBase = null) + { + Mock selfMock = BuildSelfMock( + implementationType, + enablePrivate, + mockBehavior ?? MockBehavior, + defaultValue ?? DefaultValue, + callBase ?? CallBase); + WithTypeMap(typeMap => { - DefaultValue = DefaultValue, - CallBase = CallBase - }; - return mock.Object; + typeMap[implementationType] = new MockInstance(selfMock); + typeMap[serviceType] = new MockInstance(selfMock.As(serviceType)); + }); + return selfMock.Object; } + /// + /// This constructs a self mock similar to . + /// The created mock instance is automatically registered using both its implementation and service type. + /// + /// The implementation type of the service. + /// When true, non-public constructors will also be used to create mocks. + /// Sets the Behavior property on the created Mock. + /// Sets the DefaultValue propert on the created Mock. + /// Sets the CallBase property on the created Mock. + /// An instance with virtual and abstract members mocked + public object WithSelfMock( + Type implementationType, + bool enablePrivate = false, + MockBehavior? mockBehavior = null, + DefaultValue? defaultValue = null, + bool? callBase = null) + { + Mock selfMock = BuildSelfMock( + implementationType, + enablePrivate, + mockBehavior ?? MockBehavior, + defaultValue ?? DefaultValue, + callBase ?? CallBase); + WithTypeMap(typeMap => + { + typeMap[implementationType] = new MockInstance(selfMock); + }); + return selfMock.Object; + } + + + private Mock BuildSelfMock(bool enablePrivate, MockBehavior mockBehavior, DefaultValue defaultValue, bool callBase) + where T : class? + { + var context = new ObjectGraphContext(enablePrivate); + return CreateMock(typeof(T), mockBehavior, defaultValue, callBase, context) is Mock mock + ? mock + : throw new InvalidOperationException($"Failed to create self mock of type {typeof(T).FullName}"); + } - #endregion Create Instance/SelfMock + private Mock BuildSelfMock(Type serviceType, bool enablePrivate, MockBehavior mockBehavior, DefaultValue defaultValue, bool callBase) + { + var context = new ObjectGraphContext(enablePrivate); + return CreateMock(serviceType, mockBehavior, defaultValue, callBase, context) is Mock mock + ? mock + : throw new InvalidOperationException($"Failed to create self mock of type {serviceType.FullName}"); + } + + #endregion CreateSelfMock #region Use @@ -283,15 +429,13 @@ public void Use(Type type, object service) { if (type is null) throw new ArgumentNullException(nameof(type)); if (service != null && !type.IsInstanceOfType(service)) - throw new ArgumentException($"{nameof(service)} is not of type {type}", nameof(service)); - if (TypeMap is { } typeMap) { - typeMap[type] = new RealInstance(service); + throw new ArgumentException($"{nameof(service)} is not of type {type}", nameof(service)); } - else + WithTypeMap(typeMap => { - throw new InvalidOperationException($"{nameof(CacheResolver)} was not found. Cannot cache service instance without resolver."); - } + typeMap[type] = new RealInstance(service); + }); } /// @@ -302,14 +446,10 @@ public void Use(Type type, object service) public void Use(Mock mockedService) where TService : class { - if (TypeMap is { } typeMap) + WithTypeMap(typeMap => { typeMap[typeof(TService)] = new MockInstance(mockedService ?? throw new ArgumentNullException(nameof(mockedService))); - } - else - { - throw new InvalidOperationException($"{nameof(CacheResolver)} was not found. Cannot cache service instance without resolver."); - } + }); } /// @@ -776,6 +916,30 @@ public void Verify(Expression> expression, Times ti #region Utilities + internal Mock? CreateMock(Type serviceType, MockBehavior mockBehavior, DefaultValue defaultValue, bool callBase, ObjectGraphContext objectGraphContext) + { + var mockType = typeof(Mock<>).MakeGenericType(serviceType); + + bool mayHaveDependencies = serviceType.IsClass + && !typeof(Delegate).IsAssignableFrom(serviceType); + + object?[] constructorArgs = Array.Empty(); + if (mayHaveDependencies && + TryGetConstructorInvocation(serviceType, objectGraphContext, out ConstructorInfo? ctor, out IInstance[]? arguments)) + { + constructorArgs = arguments.Select(x => x.Value).ToArray(); + CacheInstances(arguments.Zip(ctor.GetParameters(), (i, p) => (p.ParameterType, i))); + } + + if (Activator.CreateInstance(mockType, mockBehavior, constructorArgs) is Mock mock) + { + mock.DefaultValue = defaultValue; + mock.CallBase = callBase; + return mock; + } + return null; + } + internal bool TryGetConstructorInvocation( Type type, ObjectGraphContext context, @@ -828,10 +992,13 @@ private Mock GetOrMakeMockFor(Type type) if (TryResolve(type, new ObjectGraphContext(false), out IInstance? instance) && instance is MockInstance mockInstance) { - if (TypeMap is { } typeMap && !typeMap.ContainsKey(type)) + WithTypeMap(typeMap => { - typeMap[type] = mockInstance; - } + if (!typeMap.ContainsKey(type)) + { + typeMap[type] = mockInstance; + } + }); return mockInstance.Mock; } throw new ArgumentException($"{type} does not resolve to a Mock"); @@ -839,7 +1006,7 @@ private Mock GetOrMakeMockFor(Type type) internal void CacheInstances(IEnumerable<(Type, IInstance)> instances) { - if (TypeMap is { } typeMap) + WithTypeMap(typeMap => { foreach (var (type, instance) in instances) { @@ -848,6 +1015,18 @@ internal void CacheInstances(IEnumerable<(Type, IInstance)> instances) typeMap[type] = instance; } } + }); + } + + private void WithTypeMap(Action> onTypeMap) + { + if (TypeMap is { } typeMap) + { + onTypeMap(typeMap); + } + else + { + throw new InvalidOperationException($"{nameof(CacheResolver)} was not found. Cannot cache service instance without resolver."); } } diff --git a/Moq.AutoMock/IInstance.cs b/Moq.AutoMock/IInstance.cs index ba22142d..8cbe2a87 100644 --- a/Moq.AutoMock/IInstance.cs +++ b/Moq.AutoMock/IInstance.cs @@ -1,6 +1,4 @@ -using System; - -namespace Moq.AutoMock; +namespace Moq.AutoMock; internal interface IInstance { diff --git a/Moq.AutoMock/MockExtensions.cs b/Moq.AutoMock/MockExtensions.cs index fd640a7d..f4351441 100644 --- a/Moq.AutoMock/MockExtensions.cs +++ b/Moq.AutoMock/MockExtensions.cs @@ -109,4 +109,16 @@ private static LambdaExpression GetExpression(string methodName) var methodCallExpression = Expression.Call(xExpression, method, parameterExpressions); return Expression.Lambda(methodCallExpression, false, new[] { xExpression }); } + + private static Lazy AsMethod { get; } = new(() => typeof(Mock).GetMethod(nameof(Mock.As))); + + internal static Mock As(this Mock mock, Type interfaceType) + { + if (AsMethod.Value is { } method) + { + return (Mock)method.MakeGenericMethod(interfaceType) + .Invoke(mock, Array.Empty()); + } + throw new InvalidOperationException($"Could not find {typeof(Mock).FullName}.{nameof(Mock.As)} method"); + } } diff --git a/Moq.AutoMock/Resolvers/MockResolver.cs b/Moq.AutoMock/Resolvers/MockResolver.cs index d993e345..577dd234 100644 --- a/Moq.AutoMock/Resolvers/MockResolver.cs +++ b/Moq.AutoMock/Resolvers/MockResolver.cs @@ -1,8 +1,4 @@ -using System; -using System.Linq; -using System.Reflection; - -namespace Moq.AutoMock.Resolvers; +namespace Moq.AutoMock.Resolvers; /// /// A resolver that resolves requested types with Mock<T> instances. @@ -36,24 +32,13 @@ public void Resolve(MockResolutionContext context) { if (context.RequestType == typeof(string)) return; - Type requestType = context.RequestType; - var mockType = typeof(Mock<>).MakeGenericType(requestType); - - bool mayHaveDependencies = requestType.IsClass - && !typeof(Delegate).IsAssignableFrom(requestType); - - object?[] constructorArgs = Array.Empty(); - if (mayHaveDependencies && - context.AutoMocker.TryGetConstructorInvocation(requestType, context.ObjectGraphContext, out ConstructorInfo? ctor, out IInstance[]? arguments)) - { - constructorArgs = arguments.Select(x => x.Value).ToArray(); - context.AutoMocker.CacheInstances(arguments.Zip(ctor.GetParameters(), (i, p) => (p.ParameterType, i))); - } - - if (Activator.CreateInstance(mockType, _mockBehavior, constructorArgs) is Mock mock) + if (context.AutoMocker.CreateMock( + context.RequestType, + _mockBehavior, + _defaultValue, + _callBase, + context.ObjectGraphContext) is { } mock) { - mock.DefaultValue = _defaultValue; - mock.CallBase = _callBase; context.Value = mock; } }