From 214f10e2abab03374eff38ba5f4bc40fa6f759ed Mon Sep 17 00:00:00 2001 From: Chris Winland Date: Mon, 29 Jul 2024 14:04:09 -0400 Subject: [PATCH] Add SetupProperty and new CreateMockInternal --- .../Extensions/MockerCreationExtensions.cs | 107 ++++++++++++-- .../Extensions/TestClassExtensions.cs | 2 +- FastMoq.Core/FastMoq.Core.csproj | 8 +- FastMoq.Core/Mocker.cs | 135 +++++++++--------- .../MockerCreationExtensionsTests.cs | 62 ++++++++ FastMoq.Web/FastMoq.Web.csproj | 4 +- 6 files changed, 228 insertions(+), 90 deletions(-) create mode 100644 FastMoq.Tests/MockerCreationExtensionsTests.cs diff --git a/FastMoq.Core/Extensions/MockerCreationExtensions.cs b/FastMoq.Core/Extensions/MockerCreationExtensions.cs index 913fa1e..a5eccf4 100644 --- a/FastMoq.Core/Extensions/MockerCreationExtensions.cs +++ b/FastMoq.Core/Extensions/MockerCreationExtensions.cs @@ -1,6 +1,7 @@ using FastMoq.Models; using Microsoft.EntityFrameworkCore; using Moq; +using System.Linq.Expressions; using System.Reflection; namespace FastMoq.Extensions @@ -100,16 +101,28 @@ public static class MockerCreationExtensions data ); + /// + /// Creates a mock given the Type of Mock. Properties will be stubbed and have default setups. + /// + /// Type of Mock + /// The mocker. + /// if set to true, indicates if non-public constructors should be searched. + /// This is designed for interface mocks or concrete mocks without parameters. + /// Cannot create instance of Mock. + public static Mock CreateMockInternal(this Mocker mocker, bool isNonPublic = true) where T : class => + (Mock)mocker.CreateMockInternal(typeof(T), new List(), true); + /// /// Creates the mock internal. /// /// The mocker. /// The type. - /// The constructor. - /// if set to true [is non public]. - /// Creates the mock internal. - /// Cannot create instance. - public static Mock CreateMockInternal(this Mocker mocker, Type type, IReadOnlyCollection parameterList, bool isNonPublic = false) + /// The constructor parameters. + /// if set to true, indicates if non-public constructors should be searched. + /// if set to true, attempts to setup internal mocks and properties. + /// Parameter list only works if the type is concrete. Otherwise, pass an empty list. + /// Cannot create instance of Mock. + public static Mock CreateMockInternal(this Mocker mocker, Type type, IReadOnlyCollection? parameterList = null, bool isNonPublic = false, bool setupMock = true) { var flags = isNonPublic ? BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance @@ -120,18 +133,31 @@ public static Mock CreateMockInternal(this Mocker mocker, Type type, IReadOnlyCo // Execute new Mock with Loose Behavior and arguments from constructor, if applicable. var parameters = new List { mocker.Strict ? MockBehavior.Strict : MockBehavior.Loose }; + parameterList ??= new List(); parameterList.ForEach(parameters.Add); - return Activator.CreateInstance(newType, - flags, - null, - parameters.ToArray(), - null, - null - ) as Mock ?? - throw new ApplicationException("Cannot create instance."); + var instance = Activator.CreateInstance(newType, + flags, + null, + parameters.ToArray(), + null, + null + ) as Mock ?? + throw CannotCreateMock(type); + + if (setupMock) + { + mocker.SetupMock(type, instance); + } + + instance.RaiseIfNull(); + return instance; } + private static ApplicationException CannotCreateMock(Type type) + { + return new ApplicationException($"Cannot create instance of 'Mock<{type.Name}>'."); + } internal static object GetSafeMockObject(this Mocker mocker, Mock mock) { @@ -263,5 +289,60 @@ internal static ConstructorModel GetTypeConstructor(this Mocker mocker, Type typ var obj = mocker.AddInjections(info?.Invoke(newArgs.ToArray())); return mocker.InnerMockResolution ? mocker.AddProperties(type, obj) : obj; } + + /// + /// Setups the mock for given property info. + /// + /// The type of mock. + /// The mock. + /// The property information. + /// The value. + public static void SetupMockProperty(this Mock mock, PropertyInfo propertyInfo, object value) where TMock : class + { + // Create a parameter expression for the object instance of type TMock + var instanceParam = Expression.Parameter(typeof(TMock), "instance"); + + // Create an expression to access the property + var propertyAccess = Expression.Property(instanceParam, propertyInfo); + + // Create a lambda expression that represents the getter + var getterExpression = Expression.Lambda>(propertyAccess, instanceParam); + + // Set up the mock to return the provided value for the property getter + mock.Setup(getterExpression).Returns(value); + } + + public static void SetupMockProperty(this Mock mock, Expression> propertyExpression, object value) + where TMock : class + { + var propertyInfo = propertyExpression.GetPropertyInfo(); + mock.SetupMockProperty(propertyInfo, value); + } + + internal static PropertyInfo GetPropertyInfo(this Expression> propertyExpression) + { + if (propertyExpression.Body is MemberExpression memberExpression) + { + if (memberExpression.Member is PropertyInfo propertyInfo) + { + return propertyInfo; + } + } + + throw new ArgumentException("Expression is not a property access.", nameof(propertyExpression)); + } + + /// + /// Setups the mock for given property name. + /// + /// The type of the t mock. + /// The mock. + /// Name of the property. + /// The value. + public static void SetupMockProperty(this Mock mock, string propertyName, object value) where TMock : class + { + var propertyInfo = typeof(TMock).GetProperty("Headers"); + mock.SetupMockProperty(propertyInfo, value); + } } } diff --git a/FastMoq.Core/Extensions/TestClassExtensions.cs b/FastMoq.Core/Extensions/TestClassExtensions.cs index 5608986..e8244cc 100644 --- a/FastMoq.Core/Extensions/TestClassExtensions.cs +++ b/FastMoq.Core/Extensions/TestClassExtensions.cs @@ -609,7 +609,7 @@ internal static List GetTestedConstructors(this Mocker mocker, try { // Test Constructor. - var mock = mocker.CreateMockInternal(type, constructor.ParameterList); + var mock = mocker.CreateMockInternal(type, constructor.ParameterList, setupMock: false); _ = mock.Object; validConstructors.Add(constructor); } diff --git a/FastMoq.Core/FastMoq.Core.csproj b/FastMoq.Core/FastMoq.Core.csproj index 1e25237..d32d23e 100644 --- a/FastMoq.Core/FastMoq.Core.csproj +++ b/FastMoq.Core/FastMoq.Core.csproj @@ -43,15 +43,15 @@ + + + + - - - - diff --git a/FastMoq.Core/Mocker.cs b/FastMoq.Core/Mocker.cs index 2ef4a3b..6bab081 100644 --- a/FastMoq.Core/Mocker.cs +++ b/FastMoq.Core/Mocker.cs @@ -55,6 +55,9 @@ public class Mocker { #region Fields + public const string SETUP_ALL_PROPERTIES_METHOD_NAME = "SetupAllProperties"; + public const string SETUP = "Setup"; + /// /// The virtual mock file system that is used by mocker unless overridden with the property. /// @@ -191,21 +194,19 @@ public T AddInjections(T obj, Type? referenceType = null) where T : class? /// public void AddFileSystemAbstractionMapping() { - this - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - ; + AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType(); } /// @@ -574,8 +575,6 @@ public Mock CreateMockInstance(bool nonPublic = false, params object?[] ar /// Type must be a class or interface., nameof(type) public Mock CreateMockInstance(Type type, bool nonPublic = false, params object?[] args) { - const string SETUP_ALL_PROPERTIES_METHOD_NAME = "SetupAllProperties"; - if (type == null || (!type.IsClass && !type.IsInterface)) { throw new ArgumentException("Type must be a class or interface.", nameof(type)); @@ -585,18 +584,8 @@ public Mock CreateMockInstance(Type type, bool nonPublic = false, params object? var oMock = this.CreateMockInternal(type, constructor.ParameterList, nonPublic); - if (!Strict) - { - InvokeMethod(null, SETUP_ALL_PROPERTIES_METHOD_NAME, true, oMock); - - if (InnerMockResolution) - { - AddProperties(type, this.GetSafeMockObject(oMock)); - } - } - - AddInjections(this.GetSafeMockObject(oMock), GetTypeModel(type)?.InstanceType ?? type); - + SetupMock(type, oMock); + oMock.RaiseIfNull(); return oMock; } @@ -800,32 +789,6 @@ public static List GetList(int count, Func? func) => GetMock().Protected() ?.Setup>(methodOrPropertyName, args ?? Array.Empty()); - /// - /// Gets the method argument data. - /// - /// The method. - /// The data. - /// System.Nullable<System.Object>[]. - ///// method - //public object?[] GetMethodArgData(MethodInfo method, Dictionary? data = null) - //{ - // if (method == null) - // { - // throw new ArgumentNullException(nameof(method)); - // } - - // var args = new List(); - - // method.GetParameters().ToList().ForEach(p => - // args.Add(data?.Any(x => x.Key == p.ParameterType) ?? false - // ? data.First(x => x.Key == p.ParameterType).Value - // : GetParameter(p.ParameterType) - // ) - // ); - - // return args.ToArray(); - //} - /// /// Gets the method argument data. /// @@ -912,7 +875,7 @@ public Task GetMockAsync(Func, Task> mockFunc, params object?[] args) } /// - /// Gets of creates the mock of type. + /// Gets of creates the of type. /// /// The type. /// The arguments used to find the correct constructor for a class. @@ -1050,7 +1013,7 @@ public DbContextMock GetMockDbContext() where TDbContext mock.CallBase = true; } - var mockObject = mock.Object; + var mockObject = this.GetSafeMockObject(mock); initAction?.Invoke(mockObject); return mockObject; } @@ -1117,15 +1080,26 @@ public Mock GetProtectedMock(Type type, params object?[] args) } /// - /// Gets the required mock. + /// Gets the required mock that already exists. If it doesn't exist, an error is raised. /// /// The mock type, usually an interface. /// Mock. - /// type must be a class. - type - /// type must be a class. - type - public Mock GetRequiredMock(Type type) => type == null || (!type.IsClass && !type.IsInterface) - ? throw new ArgumentException("type must be a class.", nameof(type)) - : mockCollection.First(x => x.Type == type).Mock; + /// Type cannot be null. + /// Type must be a class. + /// Type must be a class. - type + /// Not Found. + public Mock GetRequiredMock(Type type) + { + ArgumentNullException.ThrowIfNull(type); + + var mock = (!type.IsClass && !type.IsInterface) + ? throw new ArgumentException("Type must be a class.", nameof(type)) + : mockCollection.First(x => x.Type == type).Mock; + + mock.RaiseIfNull(); + + return mock; + } /// /// Gets the required mock. @@ -1361,6 +1335,21 @@ internal MockModel AddMock(Mock mock, Type type, bool overwrite = false, bool no /// public void CallMethod(Delegate method, params object?[]? args) => CallMethod(method, args); + internal void AddProperty(object? obj, PropertyInfo writableProperty) + { + try + { + if (writableProperty.GetValue(obj) is null && !creatingTypeList.Contains(writableProperty.PropertyType)) + { + writableProperty.SetValue(obj, GetObject(writableProperty.PropertyType)); + } + } + catch (Exception ex) + { + ExceptionLog.Add(ex.Message); + } + } + internal List> CreateArgPairList(MethodBase info, params object?[]? args) { var paramList = info?.GetParameters().ToList() ?? new(); @@ -1617,19 +1606,23 @@ internal IInstanceModel GetTypeFromInterface() where T : class internal IInstanceModel GetTypeModel(Type type) => typeMap.TryGetValue(type, out var model) && model is not null ? model : new InstanceModel(type, this.GetTypeFromInterface(type)); - private void AddProperty(object? obj, PropertyInfo writableProperty) + internal void SetupMock(Type type, Mock oMock) { - try + if (!Strict) { - if (writableProperty.GetValue(obj) is null && !creatingTypeList.Contains(writableProperty.PropertyType)) + if (oMock.Setups.Count == 0) { - writableProperty.SetValue(obj, GetObject(writableProperty.PropertyType)); + // Only run this if there are no setups. + InvokeMethod(null, SETUP_ALL_PROPERTIES_METHOD_NAME, true, oMock); + } + + if (InnerMockResolution) + { + AddProperties(type, this.GetSafeMockObject(oMock)); } } - catch (Exception ex) - { - ExceptionLog.Add(ex.Message); - } + + AddInjections(this.GetSafeMockObject(oMock), GetTypeModel(type)?.InstanceType ?? type); } } } diff --git a/FastMoq.Tests/MockerCreationExtensionsTests.cs b/FastMoq.Tests/MockerCreationExtensionsTests.cs new file mode 100644 index 0000000..8ee736a --- /dev/null +++ b/FastMoq.Tests/MockerCreationExtensionsTests.cs @@ -0,0 +1,62 @@ +using Castle.Core.Logging; +using FastMoq.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace FastMoq.Tests +{ + public class MockerCreationExtensionsTests + { + private Mocker Mocks { get; } = new Mocker(); + + [Fact] + public void CreateMock_IActionContextAccessor_ShouldCreateMock() + { + var o = Mocks.CreateMockInternal(); + o.Should().NotBeNull(); + o.Should().BeOfType>(); + o.Object.Should().NotBeNull(); + + var p = Mocks.CreateMockInternal(); + p.Should().NotBeNull(); + p.Should().BeOfType>(); + p.Object.Should().NotBeNull(); + + var q = Mocks.CreateMockInternal(); + q.Should().NotBeNull(); + q.Should().BeOfType>(); + q.Object.Should().NotBeNull(); + + var r = Mocks.CreateMockInternal(); + r.Should().NotBeNull(); + r.Should().BeOfType>(); + r.Object.Should().NotBeNull(); + var rObj = r.Object; + rObj.Session.Should().NotBeNull(); + rObj.Items.Should().NotBeNull(); + rObj.User.Should().NotBeNull(); + rObj.Response.Should().BeNull(); + } + + [Fact] + public void SetupMockPropertyByPropertyInfo() + { + var mock = Mocks.GetMock(); + mock.SetupMockProperty(typeof(IFormFile).GetProperty("Headers"), new HeaderDictionary()); + } + + [Fact] + public void SetupMockPropertyByName() + { + var mock = Mocks.GetMock(); + mock.SetupMockProperty("Headers", new HeaderDictionary()); + } + + [Fact] + public void SetupMockPropertyByExpression() + { + var mock = Mocks.GetMock(); + mock.SetupMockProperty(x=>x.Headers, new HeaderDictionary()); + } + } +} diff --git a/FastMoq.Web/FastMoq.Web.csproj b/FastMoq.Web/FastMoq.Web.csproj index 5427a4f..2ec349d 100644 --- a/FastMoq.Web/FastMoq.Web.csproj +++ b/FastMoq.Web/FastMoq.Web.csproj @@ -42,9 +42,11 @@ + + + -