Skip to content

Commit

Permalink
Add SetupProperty and new CreateMockInternal
Browse files Browse the repository at this point in the history
  • Loading branch information
cwinland committed Jul 29, 2024
1 parent f556348 commit 214f10e
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 90 deletions.
107 changes: 94 additions & 13 deletions FastMoq.Core/Extensions/MockerCreationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FastMoq.Models;
using Microsoft.EntityFrameworkCore;
using Moq;
using System.Linq.Expressions;
using System.Reflection;

namespace FastMoq.Extensions
Expand Down Expand Up @@ -100,16 +101,28 @@ public static class MockerCreationExtensions
data
);

/// <summary>
/// Creates a mock given the <typeparam name="T">Type of Mock</typeparam>. Properties will be stubbed and have default setups.
/// </summary>
/// <typeparam name="T">Type of Mock</typeparam>
/// <param name="mocker">The mocker.</param>
/// <param name="isNonPublic">if set to <c>true</c>, indicates if non-public constructors should be searched.</param>
/// <remarks>This is designed for interface mocks or concrete mocks without parameters.</remarks>
/// <exception cref="System.ApplicationException">Cannot create instance of Mock.</exception>
public static Mock<T> CreateMockInternal<T>(this Mocker mocker, bool isNonPublic = true) where T : class =>
(Mock<T>)mocker.CreateMockInternal(typeof(T), new List<object?>(), true);

/// <summary>
/// Creates the mock internal.
/// </summary>
/// <param name="mocker">The mocker.</param>
/// <param name="type">The type.</param>
/// <param name="parameterList">The constructor.</param>
/// <param name="isNonPublic">if set to <c>true</c> [is non public].</param>
/// <returns>Creates the mock internal.</returns>
/// <exception cref="System.ApplicationException">Cannot create instance.</exception>
public static Mock CreateMockInternal(this Mocker mocker, Type type, IReadOnlyCollection<object?> parameterList, bool isNonPublic = false)
/// <param name="parameterList">The constructor parameters.</param>
/// <param name="isNonPublic">if set to <c>true</c>, indicates if non-public constructors should be searched.</param>
/// <param name="setupMock">if set to <c>true</c>, attempts to setup internal mocks and properties.</param>
/// <remarks>Parameter list only works if the type is concrete. Otherwise, pass an empty list.</remarks>
/// <exception cref="System.ApplicationException">Cannot create instance of Mock.</exception>
public static Mock CreateMockInternal(this Mocker mocker, Type type, IReadOnlyCollection<object?>? parameterList = null, bool isNonPublic = false, bool setupMock = true)
{
var flags = isNonPublic
? BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance
Expand All @@ -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<object?> { mocker.Strict ? MockBehavior.Strict : MockBehavior.Loose };
parameterList ??= new List<object>();
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)
{
Expand Down Expand Up @@ -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;
}

/// <summary>
/// Setups the mock for given property info.
/// </summary>
/// <typeparam name="TMock">The type of mock.</typeparam>
/// <param name="mock">The mock.</param>
/// <param name="propertyInfo">The property information.</param>
/// <param name="value">The value.</param>
public static void SetupMockProperty<TMock>(this Mock<TMock> 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<Func<TMock, object>>(propertyAccess, instanceParam);

// Set up the mock to return the provided value for the property getter
mock.Setup(getterExpression).Returns(value);
}

public static void SetupMockProperty<TMock>(this Mock<TMock> mock, Expression<Func<TMock, object>> propertyExpression, object value)
where TMock : class
{
var propertyInfo = propertyExpression.GetPropertyInfo();
mock.SetupMockProperty(propertyInfo, value);
}

internal static PropertyInfo GetPropertyInfo<TSource, TProperty>(this Expression<Func<TSource, TProperty>> 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));
}

/// <summary>
/// Setups the mock for given property name.
/// </summary>
/// <typeparam name="TMock">The type of the t mock.</typeparam>
/// <param name="mock">The mock.</param>
/// <param name="propertyName">Name of the property.</param>
/// <param name="value">The value.</param>
public static void SetupMockProperty<TMock>(this Mock<TMock> mock, string propertyName, object value) where TMock : class
{
var propertyInfo = typeof(TMock).GetProperty("Headers");
mock.SetupMockProperty(propertyInfo, value);
}
}
}
2 changes: 1 addition & 1 deletion FastMoq.Core/Extensions/TestClassExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ internal static List<ConstructorModel> 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);
}
Expand Down
8 changes: 4 additions & 4 deletions FastMoq.Core/FastMoq.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@
<Content Include="..\LICENSE.TXT" Link="license.txt" Pack="true" PackagePath="license.txt" />
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.*" Condition="'$(TargetFramework)' == 'net6.0'" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.*" Condition="'$(TargetFramework)' == 'net7.0'" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.*" Condition="'$(TargetFramework)' == 'net8.0'" />

<PackageReference Include="Microsoft.Extensions.Http" Version="6.*" Condition="'$(TargetFramework)' == 'net6.0'" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.*" Condition="'$(TargetFramework)' == 'net7.0'" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.*" Condition="'$(TargetFramework)' == 'net8.0'" />

<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" Condition="'$(TargetFramework)' == 'net6.0'" />
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="19.2.91" Condition="'$(TargetFramework)' == 'net7.0'"/>
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="21.*" Condition="'$(TargetFramework)' == 'net8.0'"/>
Expand Down
135 changes: 64 additions & 71 deletions FastMoq.Core/Mocker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ public class Mocker
{
#region Fields

public const string SETUP_ALL_PROPERTIES_METHOD_NAME = "SetupAllProperties";
public const string SETUP = "Setup";

/// <summary>
/// The virtual mock file system that is used by mocker unless overridden with the <see cref="Strict" /> property.
/// </summary>
Expand Down Expand Up @@ -191,21 +194,19 @@ public T AddInjections<T>(T obj, Type? referenceType = null) where T : class?
/// </summary>
public void AddFileSystemAbstractionMapping()
{
this
.AddType<IDirectory, DirectoryBase>()
.AddType<IDirectoryInfo, DirectoryInfoBase>()
.AddType<IDirectoryInfoFactory, MockDirectoryInfoFactory>()
.AddType<IDriveInfo, DriveInfoBase>()
.AddType<IDriveInfoFactory, MockDriveInfoFactory>()
.AddType<IFile, FileBase>()
.AddType<IFileInfo, FileInfoBase>()
.AddType<IFileInfoFactory, MockFileInfoFactory>()
.AddType<IFileStreamFactory, MockFileStreamFactory>()
.AddType<IFileSystem, FileSystemBase>()
.AddType<IFileSystemInfo, FileSystemInfoBase>()
.AddType<IFileSystemWatcherFactory, MockFileSystemWatcherFactory>()
.AddType<IPath, PathBase>()
;
AddType<IDirectory, DirectoryBase>()
.AddType<IDirectoryInfo, DirectoryInfoBase>()
.AddType<IDirectoryInfoFactory, MockDirectoryInfoFactory>()
.AddType<IDriveInfo, DriveInfoBase>()
.AddType<IDriveInfoFactory, MockDriveInfoFactory>()
.AddType<IFile, FileBase>()
.AddType<IFileInfo, FileInfoBase>()
.AddType<IFileInfoFactory, MockFileInfoFactory>()
.AddType<IFileStreamFactory, MockFileStreamFactory>()
.AddType<IFileSystem, FileSystemBase>()
.AddType<IFileSystemInfo, FileSystemInfoBase>()
.AddType<IFileSystemWatcherFactory, MockFileSystemWatcherFactory>()
.AddType<IPath, PathBase>();
}

/// <summary>
Expand Down Expand Up @@ -574,8 +575,6 @@ public Mock<T> CreateMockInstance<T>(bool nonPublic = false, params object?[] ar
/// <exception cref="System.ApplicationException">Type must be a class or interface., nameof(type)</exception>
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));
Expand All @@ -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<Mock>(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;
}

Expand Down Expand Up @@ -800,32 +789,6 @@ public static List<T> GetList<T>(int count, Func<T>? func) =>
GetMock<TMock>().Protected()
?.Setup<Task<TReturn>>(methodOrPropertyName, args ?? Array.Empty<object>());

/// <summary>
/// Gets the method argument data.
/// </summary>
/// <param name="method">The method.</param>
/// <param name="data">The data.</param>
/// <returns>System.Nullable&lt;System.Object&gt;[].</returns>
///// <exception cref="System.ArgumentNullException">method</exception>
//public object?[] GetMethodArgData(MethodInfo method, Dictionary<Type, object?>? data = null)
//{
// if (method == null)
// {
// throw new ArgumentNullException(nameof(method));
// }

// var args = new List<object?>();

// 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();
//}

/// <summary>
/// Gets the method argument data.
/// </summary>
Expand Down Expand Up @@ -912,7 +875,7 @@ public Task GetMockAsync<T>(Func<Mock<T>, Task> mockFunc, params object?[] args)
}

/// <summary>
/// Gets of creates the mock of <c>type</c>.
/// Gets of creates the <see cref="Mock"/> of <c>type</c>.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="args">The arguments used to find the correct constructor for a class.</param>
Expand Down Expand Up @@ -1050,7 +1013,7 @@ public DbContextMock<TDbContext> GetMockDbContext<TDbContext>() where TDbContext
mock.CallBase = true;
}

var mockObject = mock.Object;
var mockObject = this.GetSafeMockObject(mock);
initAction?.Invoke(mockObject);
return mockObject;
}
Expand Down Expand Up @@ -1117,15 +1080,26 @@ public Mock GetProtectedMock(Type type, params object?[] args)
}

/// <summary>
/// Gets the required mock.
/// Gets the required mock that already exists. If it doesn't exist, an error is raised.
/// </summary>
/// <param name="type">The mock type, usually an interface.</param>
/// <returns>Mock.</returns>
/// <exception cref="System.ArgumentException">type must be a class. - type</exception>
/// <exception cref="System.InvalidOperationException">type must be a class. - type</exception>
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;
/// <exception cref="ArgumentNullException">Type cannot be null.</exception>
/// <exception cref="System.ArgumentException">Type must be a class.</exception>
/// <exception cref="System.InvalidOperationException">Type must be a class. - type</exception>
/// <exception cref="InvalidOperationException">Not Found.</exception>
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;
}

/// <summary>
/// Gets the required mock.
Expand Down Expand Up @@ -1361,6 +1335,21 @@ internal MockModel AddMock(Mock mock, Type type, bool overwrite = false, bool no
/// <exception cref="System.ArgumentNullException" />
public void CallMethod(Delegate method, params object?[]? args) => CallMethod<object>(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<KeyValuePair<Type, object?>> CreateArgPairList(MethodBase info, params object?[]? args)
{
var paramList = info?.GetParameters().ToList() ?? new();
Expand Down Expand Up @@ -1617,19 +1606,23 @@ internal IInstanceModel GetTypeFromInterface<T>() 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<Mock>(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);
}
}
}
Loading

0 comments on commit 214f10e

Please sign in to comment.