Skip to content

Commit

Permalink
Fix Mock DbContext Async methods and SqlLite methods (#29)
Browse files Browse the repository at this point in the history
* Add Async handling

* Fix interface for original functionality while maintaining new functionality.

* Fix GetDbContext with SqlLite db
  • Loading branch information
cwinland authored Nov 7, 2023
1 parent 71e43f7 commit 364a690
Show file tree
Hide file tree
Showing 11 changed files with 454 additions and 88 deletions.
78 changes: 78 additions & 0 deletions FastMoq.Core/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace FastMoq.Extensions
{
/// <summary>
/// Class ObjectExtensions.
/// </summary>
public static class ObjectExtensions
{
/// <summary>
/// Raises if predicate is true.
/// </summary>
/// <param name="predicate">The predicate.</param>
/// <param name="name">The name.</param>
/// <param name="path">The path.</param>
/// <param name="line">The line.</param>
/// <param name="exp">The exp.</param>
/// <returns><c>true</c> if expression is true, <c>false</c> otherwise.</returns>
/// <exception cref="System.InvalidOperationException"></exception>
public static bool RaiseIf(Func<bool> predicate, string name, string path, int line, string exp) =>
predicate() ? throw new InvalidOperationException($"{exp} in {name} is invalid on line {line} of {path}") : true;

/// <summary>
/// Raises if null.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="thing">The thing.</param>
/// <param name="name">The name.</param>
/// <param name="path">The path.</param>
/// <param name="line">The line.</param>
/// <param name="exp">The exp.</param>
/// <exception cref="System.InvalidOperationException"></exception>
public static void RaiseIfNull<T>([NotNull] this T? thing, [CallerMemberName] string? name = null, [CallerFilePath] string? path = null,
[CallerLineNumber] int? line = null, [CallerArgumentExpression(nameof(thing))] string? exp = null)
where T : class
=> RaiseIf(() => thing is null, name ?? string.Empty, path ?? string.Empty, line ?? 0, exp ?? string.Empty);

/// <summary>
/// Raises if null.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="thing">The thing.</param>
/// <param name="name">The name.</param>
/// <param name="path">The path.</param>
/// <param name="line">The line.</param>
/// <param name="exp">The exp.</param>
/// <exception cref="System.InvalidOperationException"></exception>
public static void RaiseIfNull<T>([NotNull] this T? thing, [CallerMemberName] string? name = null, [CallerFilePath] string? path = null,
[CallerLineNumber] int? line = null, [CallerArgumentExpression(nameof(thing))] string? exp = null)
where T : struct
=> RaiseIf(() => thing is null, name ?? string.Empty, path ?? string.Empty, line ?? 0, exp ?? string.Empty);

/// <summary>
/// Raises if null or empty.
/// </summary>
/// <param name="thing">The thing.</param>
/// <param name="name">The name.</param>
/// <param name="path">The path.</param>
/// <param name="line">The line.</param>
/// <param name="exp">The exp.</param>
public static void RaiseIfNullOrEmpty([NotNull] this string? thing, [CallerMemberName] string? name = null,
[CallerFilePath] string? path = null, [CallerLineNumber] int? line = null, [CallerArgumentExpression(nameof(thing))] string? exp = null)
=> RaiseIf(() => string.IsNullOrEmpty(thing), name ?? string.Empty, path ?? string.Empty, line ?? 0, exp ?? string.Empty);

/// <summary>
/// Raises if null or whitespace.
/// </summary>
/// <param name="thing">The thing.</param>
/// <param name="name">The name.</param>
/// <param name="path">The path.</param>
/// <param name="line">The line.</param>
/// <param name="exp">The exp.</param>
public static void RaiseIfNullOrWhitespace([NotNull] this string? thing, [CallerMemberName] string? name = null,
[CallerFilePath] string? path = null, [CallerLineNumber] int? line = null, [CallerArgumentExpression(nameof(thing))] string? exp = null)
=> RaiseIf(() => string.IsNullOrWhiteSpace(thing), name ?? string.Empty, path ?? string.Empty, line ?? 0, exp ?? string.Empty);
}
}
106 changes: 70 additions & 36 deletions FastMoq.Core/Extensions/TestClassExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace FastMoq.Extensions
{
Expand All @@ -9,9 +10,66 @@ namespace FastMoq.Extensions
/// </summary>
public static class TestClassExtensions
{
/// <summary>
/// Calls the generic method.
/// </summary>
/// <param name="typeParameter">The type parameter.</param>
/// <param name="obj">The object.</param>
/// <param name="methodName">Name of the method.</param>
/// <param name="parameterTypes">The parameter types.</param>
/// <param name="parameters">The parameters.</param>
/// <returns>Calls the generic method.</returns>
public static object? CallGenericMethod(this Type typeParameter, object obj, [CallerMemberName] string? methodName = null,
Type[]? parameterTypes = null, object[]? parameters = null)
{
parameterTypes ??= Array.Empty<Type>();
parameters ??= Array.Empty<object>();

var method = obj.GetType().GetMethods()
.FirstOrDefault(m => m.Name == methodName &&
m.IsGenericMethodDefinition &&
m.GetParameters().Select(p => p.ParameterType).SequenceEqual(parameterTypes)
) ??
throw new MissingMethodException(obj.GetType().FullName, methodName);

var genericMethod = method.MakeGenericMethod(typeParameter);
return genericMethod.Invoke(obj, parameters);
}

public static object GetTestData(this IReadOnlyList<object>? testData, int i, ParameterInfo p) =>
testData != null && i < testData.Count ? testData[i] : p.ParameterType.GetDefaultValue();
/// <summary>
/// Ensures the null check thrown.
/// </summary>
/// <param name="action">The action.</param>
/// <param name="parameterName">Name of the parameter.</param>
/// <param name="constructorName">Name of the constructor.</param>
/// <param name="output">The output.</param>
public static void EnsureNullCheckThrown(this Action action, string parameterName, string? constructorName = "",
Action<string>? output = null)
{
try
{
output?.Invoke($"Testing {constructorName}\n - {parameterName}");

try
{
action();
}
catch (ArgumentNullException ex)
{
if (!ex.Message.Contains(parameterName))
{
throw;
}
}

output?.Invoke($"Passed {parameterName}");
}
catch
{
output?.Invoke($"Failed {parameterName}");
throw;
}
}

/// <summary>
/// Gets the default value.
Expand Down Expand Up @@ -172,6 +230,16 @@ public static string GetMemberName<T, TValue>(this T _, Expression<Func<T, TValu
where TObject : class? =>
obj.GetProperty(name)?.GetValue(obj) ?? defaultValue ?? default;

/// <summary>
/// Gets the test data.
/// </summary>
/// <param name="testData">The test data.</param>
/// <param name="i">The i.</param>
/// <param name="p">The p.</param>
/// <returns>object of the test data.</returns>
public static object GetTestData(this IReadOnlyList<object>? testData, int i, ParameterInfo p) =>
testData != null && i < testData.Count ? testData[i] : p.ParameterType.GetDefaultValue();

Check warning on line 241 in FastMoq.Core/Extensions/TestClassExtensions.cs

View workflow job for this annotation

GitHub Actions / build, pack & publish

Possible null reference return.

/// <summary>
/// Sets the field value.
/// </summary>
Expand Down Expand Up @@ -335,39 +403,5 @@ internal static bool IsValidConstructorByType(this ConstructorInfo info, params
/// <param name="type">The type.</param>
/// <exception cref="System.ArgumentException"></exception>
internal static void ThrowAlreadyExists(this Type type) => throw new ArgumentException($"{type} already exists.");

/// <summary>
/// Ensures the null check thrown.
/// </summary>
/// <param name="action">The action.</param>
/// <param name="parameterName">Name of the parameter.</param>
/// <param name="constructorName">Name of the constructor.</param>
/// <param name="output">The output.</param>
public static void EnsureNullCheckThrown(this Action action, string parameterName, string? constructorName = "", Action<string>? output = null)
{
try
{
output?.Invoke($"Testing {constructorName}\n - {parameterName}");

try
{
action();
}
catch (ArgumentNullException ex)
{
if (!ex.Message.Contains(parameterName))
{
throw;
}
}

output?.Invoke($"Passed {parameterName}");
}
catch
{
output?.Invoke($"Failed {parameterName}");
throw;
}
}
}
}
5 changes: 3 additions & 2 deletions FastMoq.Core/FastMoq.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@

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

<ItemGroup>

<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="*" />
<PackageReference Include="NuGet.Build.Tasks.Pack" Version="6.4.0">
<PrivateAssets>all</PrivateAssets>
Expand Down
1 change: 1 addition & 0 deletions FastMoq.Core/Mocker.CreateInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ public partial class Mocker
return obj;
}

// Special handling for DbContext types.
if (tType.IsAssignableTo(typeof(Microsoft.EntityFrameworkCore.DbContext)))
{
var mockObj = GetMockDbContext(tType);
Expand Down
58 changes: 36 additions & 22 deletions FastMoq.Core/Mocker.DbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
using Microsoft.EntityFrameworkCore;
using Moq;
using System.Data.Common;
using System.Linq.Expressions;
using System.Reflection;

namespace FastMoq
{
Expand All @@ -15,24 +13,49 @@ namespace FastMoq
public partial class Mocker
{
/// <summary>
/// Gets the database context using a SqlLite DB or provided options and DbConnection.
/// Gets the database context.
/// </summary>
/// <typeparam name="TContext">The type of the t context.</typeparam>
/// <param name="options">The options.</param>
/// <param name="connection">The connection.</param>
/// <returns>TContext of the database context.</returns>
/// <returns>TContext.</returns>
public TContext GetDbContext<TContext>(DbContextOptions<TContext>? options = null, DbConnection? connection = null)
where TContext : DbContext =>
GetDbContext(contextOptions =>
{
AddType(_ => contextOptions, true);
AddType<TContext>(_ => CreateInstance<TContext>(), true);
return CreateInstance<TContext>() ?? throw new InvalidOperationException("Unable to create DbContext.");
var newInstance = CreateInstanceNonPublic<TContext>() ?? throw new InvalidOperationException("Unable to create DbContext.");

AddType(_ => newInstance, true);

return newInstance;
},
options,
connection
);

/// <summary>
/// Gets the database context.
/// </summary>
/// <typeparam name="TContext">The type of the t context.</typeparam>
/// <param name="newObjectFunc">The new object function.</param>
/// <returns>TContext.</returns>
public TContext GetDbContext<TContext>(Func<DbContextOptions, TContext> newObjectFunc) where TContext : DbContext
{
DbConnection = new SqliteConnection("DataSource=:memory:");
DbConnection.Open();

var dbContextOptions = new DbContextOptionsBuilder<TContext>()
.UseSqlite(DbConnection)
.Options;

var context = newObjectFunc(dbContextOptions);
context.Database.EnsureCreated();
context.SaveChanges();

return context;
}

/// <summary>
/// Gets the database context using a SqlLite DB or provided options and DbConnection.
/// </summary>
Expand All @@ -41,8 +64,8 @@ public TContext GetDbContext<TContext>(DbContextOptions<TContext>? options = nul
/// <param name="options">The options.</param>
/// <param name="connection">The connection.</param>
/// <returns>TContext.</returns>
public TContext GetDbContext<TContext>(Func<DbContextOptions<TContext>, TContext> newObjectFunc, DbContextOptions<TContext>? options = null,
DbConnection? connection = null) where TContext : DbContext
public TContext GetDbContext<TContext>(Func<DbContextOptions<TContext>, TContext> newObjectFunc, DbContextOptions<TContext>? options,
DbConnection? connection) where TContext : DbContext
{
DbConnection = connection ?? new SqliteConnection("DataSource=:memory:");
DbConnection.Open();
Expand All @@ -64,22 +87,13 @@ public TContext GetDbContext<TContext>(Func<DbContextOptions<TContext>, TContext
/// </summary>
/// <param name="contextType">Type of the context.</param>
/// <returns>Mock of the mock database context.</returns>
/// <exception cref="System.InvalidOperationException">Unable to get MockDb. Try GetDbContext to use internal database.</exception>
/// <exception cref="NotSupportedException"></exception>
/// <exception cref="MissingMethodException">GetMockDbContext</exception>
public Mock GetMockDbContext(Type contextType)
{
if (!contextType.IsAssignableTo(typeof(DbContext)))

{
throw new NotSupportedException($"{contextType} must inherit from {typeof(DbContext).FullName}.");
}

var method = GetType().GetMethods().FirstOrDefault(x=> x.Name.Equals(nameof(GetMockDbContext)) && x.IsGenericMethodDefinition) ??
throw new MissingMethodException(GetType().FullName, nameof(GetMockDbContext));

var generic = method.MakeGenericMethod(contextType);
return (Mock)generic.Invoke(this, null);
}
public Mock GetMockDbContext(Type contextType) => contextType.CallGenericMethod(this) as Mock ??
throw new InvalidOperationException(
"Unable to get MockDb. Try GetDbContext to use internal database."
);

/// <summary>
/// Gets the mock database context.
Expand Down
Loading

0 comments on commit 364a690

Please sign in to comment.