Skip to content

Commit

Permalink
Expand TypeMap functionality for multiple parameters. Sending METADAT…
Browse files Browse the repository at this point in the history
…A to the TypeMap to allow the test to determine how to create the data for a parameter. AddProperties can now change readonly properties.
  • Loading branch information
cwinland committed Aug 1, 2024
1 parent 9e301c9 commit aa754da
Show file tree
Hide file tree
Showing 14 changed files with 335 additions and 59 deletions.
9 changes: 6 additions & 3 deletions FastMoq.Core/Extensions/MockerCreationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,10 @@ internal static ConstructorModel GetTypeConstructor(this Mocker mocker, Type typ
if (!type.IsInterface)
{
// Find the best constructor and build the parameters.
constructor = args.Length > 0 || nonPublic ? mocker.FindConstructor(type, true, args) : mocker.FindConstructor(true, type, nonPublic);
constructor = args.Length > 0 ||
nonPublic
? mocker.FindConstructor(type, true, args)
: mocker.FindConstructor(true, type, nonPublic);
}
}
catch (Exception ex)
Expand Down Expand Up @@ -231,7 +234,7 @@ internal static ConstructorModel GetTypeConstructor(this Mocker mocker, Type typ

if (type.CreateFunc != null)
{
return (T) type.CreateFunc.Invoke(mocker);
return (T?)type.CreateFunc.Invoke(mocker, type.InstanceType);
}

data ??= new();
Expand Down Expand Up @@ -282,7 +285,7 @@ internal static ConstructorModel GetTypeConstructor(this Mocker mocker, Type typ
for (var i = args.Length; i < paramList.Count; i++)
{
var p = paramList[i];
newArgs.Add(p.IsOptional ? null : mocker.GetParameter(p.ParameterType));
newArgs.Add(p.IsOptional ? null : mocker.GetParameter(p));
}
}

Expand Down
2 changes: 1 addition & 1 deletion FastMoq.Core/Extensions/TestClassExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ internal static bool CheckException(Exception verifyException, Exception? expect

method?.GetParameters().ToList().ForEach(p => args.Add(data?.Any(x => x.Key == p.ParameterType) ?? false
? data.First(x => x.Key == p.ParameterType).Value
: mocker.GetParameter(p.ParameterType)
: mocker.GetParameter(p)
)
);

Expand Down
131 changes: 109 additions & 22 deletions FastMoq.Core/Mocker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,20 @@ public MockModel<T> AddMock<T>(Mock<T> mock, bool overwrite, bool nonPublic = fa
try
{
creatingTypeList.Add(type);
var writableProperties = type.GetProperties().Where(x => x.CanWrite && x.CanRead).ToList();
var properties = type.GetProperties()
.Where(x => x.CanRead &&
(x.CanWrite || data.Any(d => d.Key.Equals(x.Name, StringComparison.OrdinalIgnoreCase))))
.ToList();

foreach (var writableProperty in writableProperties)
foreach (var property in properties)
{
AddProperty(obj, writableProperty, data);
AddProperty(obj, property, type, data);
}
}
catch (Exception ex)
{
ExceptionLog.Add(ex.GetBaseException().Message);
}
finally
{
creatingTypeList.Remove(type);
Expand All @@ -284,6 +291,34 @@ public Mocker AddType(Type tInterface, Type tClass, Func<Mocker, object>? create
ArgumentNullException.ThrowIfNull(tClass);
ArgumentNullException.ThrowIfNull(tInterface);

ValidateAndReplaceType(tInterface, tClass, replace);

typeMap.Add(tInterface, new InstanceModel(tInterface, tClass, createFunc, args?.ToList() ?? []));

return this;
}

/// <summary>
/// Adds a value to map the datatype to a value for injecting into mock in the <see cref="typeMap" />.
/// This is similar to dependency injection. It will resolve the value based on the data type.
/// </summary>
/// <remarks>This value will be used in all instances. However, other mechanisms might overwrite the value, if set.</remarks>
/// <typeparam name="T"></typeparam>
/// <param name="value">a value to inject.</param>
/// <param name="replace">Replace type if already exists. Default: false.</param>
public Mocker AddType<T>(T value, bool replace = false)
{
if (replace && typeMap.ContainsKey(typeof(T)))
{
typeMap.Remove(typeof(T));
}
typeMap.Add(typeof(T), new InstanceModel<T>(_ => value));

return this;
}

private void ValidateAndReplaceType(Type tInterface, Type tClass, bool replace)
{
if (tClass.IsInterface)
{
var message = tInterface.Name switch
Expand All @@ -306,6 +341,14 @@ _ when tInterface.Name.Equals(tClass.Name) =>
{
typeMap.Remove(tInterface);
}
}

public Mocker AddType(Type tInterface, Type tClass, Func<Mocker, object?, object>? createFunc = null, bool replace = false, params object?[]? args)
{
ArgumentNullException.ThrowIfNull(tClass);
ArgumentNullException.ThrowIfNull(tInterface);

ValidateAndReplaceType(tInterface, tClass, replace);

typeMap.Add(tInterface, new InstanceModel(tInterface, tClass, createFunc, args?.ToList() ?? []));

Expand All @@ -323,6 +366,28 @@ _ when tInterface.Name.Equals(tClass.Name) =>
public Mocker AddType<T>(Func<Mocker, T>? createFunc = null, bool replace = false, params object?[]? args) where T : class =>
AddType<T, T>(createFunc, replace, args);

/// <summary>
/// Adds an interface to Class mapping to the <see cref="typeMap" /> for easier resolution.
/// This is similar to dependency injection. It will resolve an interface to the specified concrete class.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="createFunc">An optional create function used to create the class.</param>
/// <param name="replace">Replace type if already exists. Default: false.</param>
/// <param name="args">arguments needed in model.</param>
public Mocker AddType<T>(Func<Mocker, object?, T>? createFunc = null, bool replace = false, params object?[]? args) where T : class =>
AddType<T, T>(createFunc, replace, args);

/// <summary>
/// Adds an interface to Class mapping to the <see cref="typeMap" /> for easier resolution.
/// This is similar to dependency injection. It will resolve an interface to the specified concrete class.
/// </summary>
/// <typeparam name="TInterface"></typeparam>
/// <typeparam name="TClass"></typeparam>
/// <param name="replace">Replace type if already exists. Default: false.</param>
/// <param name="args">arguments needed in model.</param>
public Mocker AddType<TInterface, TClass>(bool replace = false, params object?[]? args)
where TInterface : class where TClass : class => AddType(typeof(TInterface), typeof(TClass), (Func<Mocker, TClass>?)null, replace, args);

/// <summary>
/// Adds an interface to Class mapping to the <see cref="typeMap" /> for easier resolution.
/// This is similar to dependency injection. It will resolve an interface to the specified concrete class.
Expand All @@ -334,7 +399,10 @@ public Mocker AddType<T>(Func<Mocker, T>? createFunc = null, bool replace = fals
/// <param name="args">arguments needed in model.</param>
/// <exception cref="ArgumentException">$"{typeof(TClass).Name} cannot be an interface."</exception>
/// <exception cref="ArgumentException">$"{typeof(TClass).Name} is not assignable to {typeof(TInterface).Name}."</exception>
public Mocker AddType<TInterface, TClass>(Func<Mocker, TClass>? createFunc = null, bool replace = false, params object?[]? args)
public Mocker AddType<TInterface, TClass>(Func<Mocker, TClass>? createFunc, bool replace = false, params object?[]? args)
where TInterface : class where TClass : class => AddType(typeof(TInterface), typeof(TClass), createFunc, replace, args);

public Mocker AddType<TInterface, TClass>(Func<Mocker, object?, TClass>? createFunc, bool replace = false, params object?[]? args)
where TInterface : class where TClass : class => AddType(typeof(TInterface), typeof(TClass), createFunc, replace, args);

/// <summary>
Expand Down Expand Up @@ -446,7 +514,7 @@ private bool TryGetModelInstance(IInstanceModel typeInstanceModel, Type tType, o
try
{
ConstructorHistory.AddOrUpdate(tType, typeInstanceModel);
obj = typeInstanceModel.CreateFunc(this);
obj = typeInstanceModel.CreateFunc.Invoke(this, tType);

Check warning on line 517 in FastMoq.Core/Mocker.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Converting null literal or possible null value to non-nullable type.
}
finally
{
Expand Down Expand Up @@ -497,7 +565,7 @@ private bool TryGetModelInstance(IInstanceModel typeInstanceModel, Type tType, o
var type = typeof(T).IsInterface ? GetTypeFromInterface<T>() : new InstanceModel<T>();

return type.CreateFunc != null
? (T) type.CreateFunc(this)
? (T) type.CreateFunc.Invoke(this, type.InstanceType)

Check warning on line 568 in FastMoq.Core/Mocker.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Converting null literal or possible null value to non-nullable type.
: CreateInstanceNonPublic(type.InstanceType, args) as T;
}

Expand Down Expand Up @@ -808,7 +876,7 @@ public static List<T> GetList<T>(int count, Func<T>? func) =>
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)
: GetParameter(p)
)
);

Expand Down Expand Up @@ -946,14 +1014,14 @@ public DbContextMock<TDbContext> GetMockDbContext<TDbContext>() where TDbContext

try
{
return !MockOptional && info.IsOptional ? null : GetParameter(info.ParameterType);
}
catch (FileNotFoundException ex)
{
ExceptionLog.Add(ex.Message);
throw;
return (!MockOptional && info.IsOptional) switch
{
true when info.HasDefaultValue => info.DefaultValue,
true => null,
_ => GetParameter(info),
};
}
catch (AmbiguousImplementationException ex)
catch (Exception ex) when (ex is FileNotFoundException or AmbiguousImplementationException)
{
ExceptionLog.Add(ex.Message);
throw;
Expand Down Expand Up @@ -987,7 +1055,7 @@ public DbContextMock<TDbContext> GetMockDbContext<TDbContext>() where TDbContext
if (typeValueModel.CreateFunc != null)
{
// If a create function is provided, use it instead of a mock object.
return AddInjections(typeValueModel.CreateFunc.Invoke(this), typeValueModel.InstanceType);
return AddInjections(typeValueModel.CreateFunc.Invoke(this, typeValueModel.InstanceType), typeValueModel.InstanceType);
}

if (!Strict)
Expand Down Expand Up @@ -1335,7 +1403,7 @@ 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, params KeyValuePair<string, object>[] data)
internal void AddProperty(object? obj, PropertyInfo writableProperty, Type objType, params KeyValuePair<string, object>[] data)
{
try
{
Expand All @@ -1354,7 +1422,23 @@ value is null ||
value = data.Any(x => x.Key.Contains(writableProperty.Name, StringComparison.OrdinalIgnoreCase))
? data.First(x => x.Key.Contains(writableProperty.Name, StringComparison.OrdinalIgnoreCase)).Value
: GetObject(writableProperty.PropertyType);
writableProperty.SetValue(obj, value);
if (writableProperty.CanWrite)
{
writableProperty.SetValue(obj, value);
}
else
{
// Access the backing field directly if the property is read-only
var backingField = objType.GetField($"<{writableProperty.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
if (backingField != null)
{
backingField.SetValue(obj, value);
}
else
{
ExceptionLog.Add($"Backing field for property '{writableProperty.Name}' not found.");
}
}
}
}
catch (Exception ex)
Expand All @@ -1377,7 +1461,7 @@ value is null ||
{
_ when i < args.Length => args[i],
_ when p.IsOptional => null,
_ => GetParameter(p.ParameterType),
_ => GetParameter(p),
};

newArgs.Add(new (p.ParameterType, val));
Expand Down Expand Up @@ -1576,11 +1660,14 @@ internal MockModel<T> GetMockModel<T>(Mock<T>? mock = null, bool autoCreate = tr
/// <summary>
/// Gets the parameter.
/// </summary>
/// <param name="parameterType">Type of the parameter.</param>
/// <param name="parameter"><see cref="ParameterInfo"/>.</param>
/// <returns>object?.</returns>
internal object? GetParameter(Type parameterType)
internal object? GetParameter(ParameterInfo parameter)
{
if (!parameterType.IsClass && !parameterType.IsInterface)
var parameterType = parameter.ParameterType;


if (!typeMap.ContainsKey(parameterType) && !parameterType.IsClass && !parameterType.IsInterface)
{
return parameterType.GetDefaultValue();
}
Expand All @@ -1589,7 +1676,7 @@ internal MockModel<T> GetMockModel<T>(Mock<T>? mock = null, bool autoCreate = tr

if (typeValueModel.CreateFunc != null)
{
return typeValueModel.CreateFunc.Invoke(this);
return typeValueModel.CreateFunc.Invoke(this, parameter);
}

return !parameterType.IsSealed ? GetObject(parameterType) : parameterType.GetDefaultValue();
Expand Down
2 changes: 1 addition & 1 deletion FastMoq.Core/Models/IInstanceModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public interface IInstanceModel : IHistoryModel
/// Gets the create function.
/// </summary>
/// <value>The create function.</value>
Func<Mocker, object>? CreateFunc { get; }
InstanceFunction? CreateFunc { get; }

/// <summary>
/// Gets the type of the instance.
Expand Down
94 changes: 94 additions & 0 deletions FastMoq.Core/Models/InstanceFunction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
namespace FastMoq.Models
{
public class InstanceFunction
{
public Type InstanceType { get; }

public InstanceFunction(Type type)
{
this.InstanceType = type;
}

// Property to hold the function
public Delegate? Function { get; set; }

// Method to set the function
public void SetFunction(Delegate function)
{
this.Function = function;
}

public object? Invoke(Mocker mocker, params object?[]? parameters)
{
var functionType = Function.GetType();

Check warning on line 23 in FastMoq.Core/Models/InstanceFunction.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Dereference of a possibly null reference.
var genericArguments = functionType.GenericTypeArguments;

return genericArguments.Length switch
{
2 => InvokeSingleParamFunction(mocker),
3 => InvokeDoubleParamFunction(mocker, parameters),
_ => throw new InvalidOperationException("No valid function provided or incorrect number of parameters.")
};
}

private object? InvokeSingleParamFunction(Mocker mocker)
{
var method = Function.GetType().GetMethod("Invoke");

Check warning on line 36 in FastMoq.Core/Models/InstanceFunction.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Dereference of a possibly null reference.
if (method != null)
{
return method.Invoke(Function, new object[] { mocker });
}

throw new InvalidOperationException("Function is not a valid single parameter function.");
}

private object? InvokeDoubleParamFunction(Mocker mocker, object?[]? parameters)
{
var method = Function.GetType().GetMethod("Invoke");

Check warning on line 47 in FastMoq.Core/Models/InstanceFunction.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Dereference of a possibly null reference.
if (method != null)
{
return method.Invoke(Function, new object[] { mocker, parameters?[0] ?? null });

Check warning on line 50 in FastMoq.Core/Models/InstanceFunction.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Possible null reference assignment.
}

throw new InvalidOperationException("Function is not a valid double parameter function.");
}


// Overloaded CreateInstance for singleParamFunc
public static InstanceFunction CreateInstance(Type type)
{
return new InstanceFunction(type);
}
}

public class InstanceFunction<T> : InstanceFunction
{
// Constructor
public InstanceFunction() : base(typeof(T)) { }

public InstanceFunction(Func<Mocker, T?> singleParamFunc) : base(typeof(T))
{
Function = singleParamFunc;
}

public InstanceFunction(Func<Mocker, object?, T?> doubleParamFunc) : base(typeof(T))
{
Function = doubleParamFunc;
}

public new T? Invoke(Mocker mocker, params object?[]? parameters)
{
if (Function is Func<Mocker, T?> singleParamFunc && (parameters == null || parameters.Length == 0))
{
return singleParamFunc(mocker);
}

if (Function is Func<Mocker, object?, T?> doubleParamFunc && parameters?.Length == 1)
{
return doubleParamFunc(mocker, parameters[0]);
}

throw new InvalidOperationException("No valid function provided or incorrect number of parameters.");
}
}
}
Loading

0 comments on commit aa754da

Please sign in to comment.