Skip to content

Commit

Permalink
feat: Support for mimicking classes (#4)
Browse files Browse the repository at this point in the history
Adds support for mimicking classes along with the previously supported
interfaces provided the class meets some criteria; non-static,
non-sealed and accessible by the proxy generator.

Example usage (when class has a parameterless constructor):
```cs
var mimickedClass = new Mimic<ClassWithParameterlessConstructor>();
```

Example usage (when class needs constructor arguments):
```cs
var mimickedClass = new Mimic<ClassWithSpecificConstructor>
{
    ConstructorArguments = [1, "arg2", 3m, new object()]
};
```
  • Loading branch information
DrBarnabus committed Feb 22, 2024
1 parent 6fdddd7 commit 26d73e9
Show file tree
Hide file tree
Showing 19 changed files with 296 additions and 42 deletions.
15 changes: 15 additions & 0 deletions src/Mimic.UnitTests/Core/Extensions/TypeExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ namespace Mimic.UnitTests.Core.Extensions;

public class TypeExtensionsTests
{
[Theory]
[InlineData(typeof(IInterface), true)]
[InlineData(typeof(AbstractClass), true)]
[InlineData(typeof(RegularClass), true)]
[InlineData(typeof(SealedClass), false)]
public void CanBeMimicked_ReturnsExpectedResultForType(Type type, bool expectedResult)
{
type.CanBeMimicked().ShouldBe(expectedResult);
}

[Theory]
[InlineData(typeof(bool), default(bool))]
[InlineData(typeof(byte), default(byte))]
Expand Down Expand Up @@ -154,4 +164,9 @@ public void CompareWith_WithGenericMatchers_ShouldReturnCorrectResult(Type[] typ
private class A;
private class B : A;
private class C;

private interface IInterface;
private abstract class AbstractClass;
private class RegularClass;
private sealed class SealedClass;
}
87 changes: 79 additions & 8 deletions src/Mimic.UnitTests/Proxy/ProxyGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Reflection;
using AutoFixture;
using Mimic.Core;
using Mimic.Exceptions;
using Mimic.Proxy;
using Mimic.Setup;

Expand All @@ -16,25 +17,88 @@ public void Instance_ShouldAlwaysReturnTheSameReference()
}

[Fact]
public void GenerateProxy_WhenCalledWithNonInterfaceType_ShouldThrowAnAssertionException()
public void GenerateProxy_WhenCalledWithInterfaceType_ShouldReturnNonNullTypeThatInheritsInterface()
{
var interceptor = new InterceptorFixture();

var ex = Should.Throw<Guard.AssertionException>(() =>
ProxyGenerator.Instance.GenerateProxy(typeof(string), Type.EmptyTypes, interceptor));
object result = ProxyGenerator.Instance.GenerateProxy(typeof(IA), Type.EmptyTypes, Array.Empty<object>(), interceptor);

ex.Expression.ShouldBe("mimicType.IsInterface");
result.ShouldNotBeNull();
result.ShouldBeAssignableTo<IA>();
}

[Fact]
public void GenerateProxy_WhenCalledWithInterfaceType_ShouldReturnNonNullTypeThatInheritsInterface()
public void GenerateProxy_WhenCalledWithSealedClassType_ShouldThrowMimicException()
{
var interceptor = new InterceptorFixture();

var ex = Should.Throw<MimicException>(() =>
ProxyGenerator.Instance.GenerateProxy(typeof(string), Type.EmptyTypes, Array.Empty<object>(), interceptor));

ex.ShouldNotBeNull();
ex.Message.ShouldBe("Type string cannot be mimicked. It must be an interface or a non-sealed/non-static class.");
ex.InnerException.ShouldNotBeNull();
ex.InnerException.ShouldBeOfType<TypeLoadException>();
}

[Fact]
public void GenerateProxy_WhenCalledWithStaticClassType_ShouldThrowMimicException()
{
var interceptor = new InterceptorFixture();

// Note: This exception comes from Castle.Core so we're just verifying it is indeed thrown and it get's wrapped
var ex = Should.Throw<MimicException>(() =>
ProxyGenerator.Instance.GenerateProxy(typeof(TypeExtensions), Type.EmptyTypes, Array.Empty<object>(), interceptor));

ex.ShouldNotBeNull();
ex.Message.ShouldBe("Type TypeExtensions cannot be mimicked. It must be an interface or a non-sealed/non-static class.");
ex.InnerException.ShouldNotBeNull();
ex.InnerException.Message.ShouldBe("Parent does not have a default constructor. The default constructor must be explicitly defined.");
}

[Fact]
public void GenerateProxy_WhenCalledWithAbstractClassType_ButCallingParameterlessConstructorThatDoesNotExist_ShouldThrowMimicException()
{
var interceptor = new InterceptorFixture();

object result = ProxyGenerator.Instance.GenerateProxy(typeof(IA), Type.EmptyTypes, interceptor);
// Note: This exception comes from Castle.Core so we're just verifying it is indeed thrown and it get's wrapped
var ex = Should.Throw<MimicException>(() =>
ProxyGenerator.Instance.GenerateProxy(typeof(C), Type.EmptyTypes, Array.Empty<object>(), interceptor));

ex.ShouldNotBeNull();
ex.Message.ShouldBe("Unable to find a constructor in type ProxyGeneratorTests.C matching given constructor arguments.");
ex.InnerException.ShouldNotBeNull();
ex.InnerException.Message.ShouldContain("Could not find a parameterless constructor.");
}

[Theory]
[AutoData]
public void GenerateProxy_WhenCalledWithAbstractClassType_ButCallingConstructorThatDoesNotExist_ShouldThrowMimicException(
string sValue)
{
var interceptor = new InterceptorFixture();

// Note: This exception comes from Castle.Core so we're just verifying it is indeed thrown and it get's wrapped
var ex = Should.Throw<MimicException>(() =>
ProxyGenerator.Instance.GenerateProxy(typeof(C), Type.EmptyTypes, [sValue], interceptor));

ex.ShouldNotBeNull();
ex.Message.ShouldBe("Unable to find a constructor in type ProxyGeneratorTests.C matching given constructor arguments.");
ex.InnerException.ShouldNotBeNull();
ex.InnerException.Message.ShouldContain("Could not find a constructor that would match given arguments:");
}

[Theory]
[AutoData]
public void GenerateProxy_WhenCalledWithAbstractClassType_ShouldReturnNonNullTypeThatInheritsParentClass(
string sValue, int iValue)
{
var interceptor = new InterceptorFixture();

object result = ProxyGenerator.Instance.GenerateProxy(typeof(C), Type.EmptyTypes, [sValue, iValue], interceptor);

result.ShouldNotBeNull();
result.ShouldBeAssignableTo<IA>();
result.ShouldBeAssignableTo<C>();
}

public class InvocationTests
Expand All @@ -45,7 +109,7 @@ public class InvocationTests
public InvocationTests()
{
_interceptor = new InterceptorFixture();
_proxyObject = (IA)ProxyGenerator.Instance.GenerateProxy(typeof(IA), Type.EmptyTypes, _interceptor);
_proxyObject = (IA)ProxyGenerator.Instance.GenerateProxy(typeof(IA), Type.EmptyTypes, Array.Empty<object>(), _interceptor);
}

[Fact]
Expand Down Expand Up @@ -322,4 +386,11 @@ public interface IA

public enum E { None = 0, One = 1, Two = 2 }
}

public abstract class C
{
public C(string sValue, int iValue)
{
}
}
}
3 changes: 2 additions & 1 deletion src/Mimic.UnitTests/Setup/Fluent/SequenceSetupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,8 @@ private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> ex
return new MethodCallSetup(methodCallExpression, mimic, methodExpectation, null);
}

private interface ISubject
// ReSharper disable once MemberCanBePrivate.Global
internal interface ISubject
{
public void MethodWithNoParameters();
public void MethodWithParameters(int v1);
Expand Down
3 changes: 2 additions & 1 deletion src/Mimic.UnitTests/Setup/Fluent/SequenceSetupTests`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,8 @@ private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> ex
return new MethodCallSetup(methodCallExpression, mimic, methodExpectation, null);
}

private interface ISubject
// ReSharper disable once MemberCanBePrivate.Global
internal interface ISubject
{
public string MethodWithNoParameters();
public string MethodWithParameters(int v1);
Expand Down
2 changes: 1 addition & 1 deletion src/Mimic.UnitTests/Setup/Fluent/SetterSetupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void Callback_WithAction_ShouldCorrectlySetCallbackBehaviour()

private static MethodCallSetup ToMethodCallSetup(Action<ISubject> setterExpression)
{
var expression = SetterExpressionConstructor.ConstructFromAction(setterExpression);
var expression = SetterExpressionConstructor.ConstructFromAction(setterExpression, null);

var mimic = new Mimic<ISubject>();
var methodCallExpression = (MethodCallExpression)expression.Body;
Expand Down
3 changes: 2 additions & 1 deletion src/Mimic.UnitTests/Setup/Fluent/SetupTests`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,8 @@ private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> ex
return new MethodCallSetup(methodCallExpression, mimic, methodExpectation, null);
}

private interface ISubject
// ReSharper disable once MemberCanBePrivate.Global
internal interface ISubject
{
public void MethodWithNoParameters();
public void MethodWithParameters(int v1);
Expand Down
3 changes: 2 additions & 1 deletion src/Mimic.UnitTests/Setup/Fluent/SetupTests`2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,8 @@ private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> ex
return new MethodCallSetup(methodCallExpression, mimic, methodExpectation, null);
}

private interface ISubject
// ReSharper disable once MemberCanBePrivate.Global
internal interface ISubject
{
public string MethodWithNoParameters();
public string MethodWithParameters(int v1);
Expand Down
3 changes: 2 additions & 1 deletion src/Mimic.UnitTests/Setup/MethodCallSetupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,8 @@ public void AddNoOpBehaviour_ShouldCorrectlyPerformNoOpOnExecution(string messag
return (setup, mimic, methodCallExpression, methodExpectation);
}

private interface ISubject
// ReSharper disable once MemberCanBePrivate.Global
internal interface ISubject
{
public void BasicVoidMethod(int iValue, string sValue, double dValue, List<bool> bValues);

Expand Down
102 changes: 100 additions & 2 deletions src/Mimic.UnitTests/Setup/MethodExpectationTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq.Expressions;
using Mimic.Exceptions;
using Mimic.Setup;

namespace Mimic.UnitTests.Setup;
Expand All @@ -20,6 +21,61 @@ public void Constructor_WhenArgumentsIsNull_ShouldSuccessfullyConstruct()
expectation.Arguments.Length.ShouldBe(0);
}

[Fact]
public void Constructor_WithNonOverridableStaticMethod_ShouldThrowUnsupportedExpressionException()
{
var method = typeof(Subject).GetMethod(nameof(Subject.StaticMethod))!;
LambdaExpression expression = (Subject _) => Subject.StaticMethod();
var arguments = ((MethodCallExpression)expression.Body).Arguments;

var ex = Should.Throw<UnsupportedExpressionException>(() => new MethodExpectation(expression, method, arguments, true));
ex.Reason.ShouldBe(UnsupportedExpressionException.UnsupportedReason.MemberIsStatic);
}

[Fact]
public void Constructor_WithNonOverridableExtensionMethod_ShouldThrowUnsupportedExpressionException()
{
var method = typeof(SubjectExtensions).GetMethod(nameof(SubjectExtensions.ExtensionMethod))!;
LambdaExpression expression = (ISubject subject) => subject.ExtensionMethod();
var arguments = ((MethodCallExpression)expression.Body).Arguments;

var ex = Should.Throw<UnsupportedExpressionException>(() => new MethodExpectation(expression, method, arguments, true));
ex.Reason.ShouldBe(UnsupportedExpressionException.UnsupportedReason.MemberIsExtension);
}

[Fact]
public void Constructor_WithNonVirtualMethod_ShouldThrowUnsupportedExpressionException()
{
var method = typeof(AbstractSubject).GetMethod(nameof(AbstractSubject.RegularMethod))!;
LambdaExpression expression = (AbstractSubject subject) => subject.RegularMethod();
var arguments = ((MethodCallExpression)expression.Body).Arguments;

var ex = Should.Throw<UnsupportedExpressionException>(() => new MethodExpectation(expression, method, arguments, true));
ex.Reason.ShouldBe(UnsupportedExpressionException.UnsupportedReason.MemberIsNotOverridable);
}

[Fact]
public void Constructor_WithSealedVirtualMethod_ShouldThrowUnsupportedExpressionException()
{
var method = typeof(ConcreteSubject).GetMethod(nameof(ConcreteSubject.VirtualMethod))!;
LambdaExpression expression = (ConcreteSubject subject) => subject.VirtualMethod();
var arguments = ((MethodCallExpression)expression.Body).Arguments;

var ex = Should.Throw<UnsupportedExpressionException>(() => new MethodExpectation(expression, method, arguments, true));
ex.Reason.ShouldBe(UnsupportedExpressionException.UnsupportedReason.MemberIsNotOverridable);
}

[Fact]
public void Constructor_WithInaccessibleMethod_ShouldThrowMimicException()
{
var method = typeof(NonAccessibleSubject).GetMethod(nameof(NonAccessibleSubject.NonAccessibleMethod))!;
LambdaExpression expression = (NonAccessibleSubject subject) => subject.NonAccessibleMethod();
var arguments = ((MethodCallExpression)expression.Body).Arguments;

var ex = Should.Throw<MimicException>(() => new MethodExpectation(expression, method, arguments, true));
ex.Message.ShouldStartWith("Method NonAccessibleMethod in type MethodExpectationTests.NonAccessibleSubject cannot be setup because it is not accessible by our proxy generator (Castle.DynamicProxy). Message returned from proxy generator: ");
}

[Fact]
public void Constructor_WhenArgumentsProvidedWithSkipMatchersTrue_ShouldSuccessfullyConstruct()
{
Expand Down Expand Up @@ -192,7 +248,8 @@ private static MethodExpectation ConstructMethodExpectation(Expression<Action<IS
return new MethodExpectation(expression, methodCallExpression.Method, methodCallExpression.Arguments);
}

private interface ISubject
// ReSharper disable once MemberCanBePrivate.Global
internal interface ISubject
{
public void MethodWithNoArguments();

Expand All @@ -203,8 +260,11 @@ private interface ISubject
public void GenericWithArguments<T>(T value, double doubleValue);
}

private class Subject : ISubject
// ReSharper disable once MemberCanBePrivate.Global
internal class Subject : ISubject
{
public static void StaticMethod() => throw new NotSupportedException();

public void MethodWithNoArguments() => throw new NotSupportedException();

public void MethodWithArguments(int intValue, string stringValue) => throw new NotSupportedException();
Expand All @@ -213,4 +273,42 @@ private class Subject : ISubject

public void GenericWithArguments<T>(T value, double doubleValue) => throw new NotSupportedException();
}

// ReSharper disable once MemberCanBePrivate.Global
internal abstract class AbstractSubject
{
public abstract void AbstractMethod();

public virtual void VirtualMethod()
{
}

public void RegularMethod()
{
}
}

// ReSharper disable once MemberCanBePrivate.Global
internal class ConcreteSubject : AbstractSubject
{
public override void AbstractMethod()
{
}

public sealed override void VirtualMethod()
{
}
}

private abstract class NonAccessibleSubject
{
public virtual void NonAccessibleMethod()
{
}
}
}

file static class SubjectExtensions
{
public static void ExtensionMethod(this MethodExpectationTests.ISubject subject) => subject.MethodWithNoArguments();
}
2 changes: 2 additions & 0 deletions src/Mimic/Core/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

internal static class TypeExtensions
{
internal static bool CanBeMimicked(this Type type) => type is { IsInterface: true } or { IsClass: true, IsSealed: false };

internal static object? GetDefaultValue(this Type type) => type.IsValueType ? Activator.CreateInstance(type) : null;

internal static bool CompareWith(this Type[] types, Type[] otherTypes)
Expand Down
9 changes: 9 additions & 0 deletions src/Mimic/Exceptions/MimicException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont
info.AddValue(nameof(Identifier), Identifier);
}

internal static MimicException TypeCannotBeMimicked(Type type, Exception? innerException = null) =>
new($"Type {TypeNameFormatter.GetFormattedName(type)} cannot be mimicked. It must be an interface or a non-sealed/non-static class.", innerException);

internal static MimicException NoConstructorWithMatchingArguments(Type type, Exception? innerException = null) =>
new ($"Unable to find a constructor in type {TypeNameFormatter.GetFormattedName(type)} matching given constructor arguments.", innerException);

internal static MimicException MethodNotAccessibleByProxyGenerator(MethodInfo method, string messageFromProxyGenerator) =>
new($"Method {method.Name} in type {TypeNameFormatter.GetFormattedName(method.DeclaringType!)} cannot be setup because it is not accessible by our proxy generator (Castle.DynamicProxy). Message returned from proxy generator: {messageFromProxyGenerator}");

internal static MimicException UnmatchableArgumentMatcher(Expression argumentExpression, Type expectedType)
{
string formattedFromType = TypeNameFormatter.GetFormattedName(argumentExpression.Type);
Expand Down
7 changes: 6 additions & 1 deletion src/Mimic/Exceptions/UnsupportedExpressionException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ public UnsupportedExpressionException(Expression expression, UnsupportedReason r
: base($"Expression ({expression}) is currently unsupported. Reason: {reason}")
{
Expression = expression;
Reason = reason;
}

public UnsupportedExpressionException(Expression expression, string expressionRepresentation, UnsupportedReason reason = UnsupportedReason.Unknown)
: base($"Expression ({expressionRepresentation}) is unsupported. Reason: {reason}")
{
Expression = expression;
Reason = reason;
}

public override void GetObjectData(SerializationInfo info, StreamingContext context)
Expand All @@ -35,6 +37,9 @@ public enum UnsupportedReason : byte
Unknown,
MemberNotInterceptable,
ExpressionThrewAnException,
UnableToDetermineArgumentMatchers
UnableToDetermineArgumentMatchers,
MemberIsStatic,
MemberIsExtension,
MemberIsNotOverridable
}
}

0 comments on commit 26d73e9

Please sign in to comment.