Skip to content

Commit

Permalink
feat: Support for Proceed()'ing to base implementation (#5)
Browse files Browse the repository at this point in the history
Adds support for proceeding to the base implementation of a non-abstract
method in a mimicked class.

Example usage:
```cs
var mimickedClass = new Mimic<AbstractClass>();

mimickedClass.Setup(m => m.NonAbstractMethod())
    .Proceed();

mimickedClass.Setup(m => m.NonAbstractMethod())
    .Callback(() => Console.WriteLine("Still hit's callbacks!"))
    .Proceed();
```
  • Loading branch information
DrBarnabus committed Feb 27, 2024
1 parent 26d73e9 commit 8d3cc63
Show file tree
Hide file tree
Showing 32 changed files with 376 additions and 48 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ to change without warning between versions until the v1 release.
## What is Mimic

**Mimic** is a friendly and familiar mocking library built for modern .NET built on top of the [Castle Project][castle]'s
dynamic proxy generator. It's simple, intuitive and type-safe API for configuring mimic's of interfaces allows for both;
Setup of return values for methods/properties and verifying if method calls have been received after the fact.
dynamic proxy generator. It's simple, intuitive and type-safe API for configuring mimic's of interfaces/classes allows
for both; Setup of return values for methods/properties and verifying if method calls have been received after the fact.

```csharp
var mimic = new Mimic<ITypeToMimic>();
Expand All @@ -38,13 +38,13 @@ mimic.VerifyReceived(m => m.IsMimicEasyToUse("it's so intuitive"), CallCount.AtL
## Features

- A friendly interface designed to ease adoption by users of other popular .NET mocking libraries
- Support for generating mock objects of interfaces (**_Coming Soon_**: and overridable members in classes)
- Support for generating mock objects of interfaces and overridable members in classes
- Intuitive and type-safe expression based API for setups and verification of methods
- Mimic is **strict by default**, meaning it throws for methods without a corresponding setup, but it's possible to
disable the default behaviour by setting `Strict = false` on construction
- Quick and easy stubbing of properties to store and retrieve values
- Comprehensive set of behaviours for method setups such as; `Returns`, `Throws`, `Callback`, `When`, `Limit`,
`Expected` and `AsSequence`
`Expected`, `AsSequence` and `Proceed`
- Verification of expected, setup and received calls including asserting no additional calls

## Roadmap
Expand All @@ -53,7 +53,6 @@ mimic.VerifyReceived(m => m.IsMimicEasyToUse("it's so intuitive"), CallCount.AtL
Considering = ❓ | Planned = 📅 | In-Progress = 🚧
```

- [🚧] Mimic of classes, specifically overridable members within classes, with support for calling base implementations
- [📅] Implicit mimicking of nested setups (e.g. `m => m.MethodThatReturnsInterface().MethodOnThatInterface()`)
- [📅] Delay behaviour (or Extension to `Returns`/`Throws`) for setups that allows for specific or random delays in
execution time
Expand Down
30 changes: 30 additions & 0 deletions src/Mimic.Sandbox/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,35 @@

mimic.VerifyExpectedReceived();

ExampleUsingAnAbstractClass();

static void SetupRef(Mimic<ITypeToMimic> mimic1)
{
string outValue = "OutValue?";
mimic1.Setup(m => m.Ref(ref Arg.Ref<int>.Any, out outValue));
}

static void ExampleUsingAnAbstractClass()
{
var mimic = new Mimic<AbstractClass>();

mimic.Setup(m => m.VirtualVoidMethod()).AsSequence()
.Next()
.Proceed()
.Next();

mimic.Setup(m => m.VirtualStringMethod())
.Proceed();

var mimickedObject = mimic.Object;

mimickedObject.VirtualVoidMethod();
mimickedObject.VirtualVoidMethod();
mimickedObject.VirtualVoidMethod();

Console.WriteLine($"Result from {nameof(AbstractClass.VirtualStringMethod)}: {mimickedObject.VirtualStringMethod()}");
}

public interface ITypeToMimic
{
string Property { get; set; }
Expand All @@ -103,3 +126,10 @@ public interface ITypeToMimic

void Ref(ref int reference, out string outValue);
}

public abstract class AbstractClass
{
public virtual void VirtualVoidMethod() => Console.WriteLine($"Hello from {nameof(VirtualVoidMethod)}");

public virtual string VirtualStringMethod() => "ValueFromBase";
}
4 changes: 4 additions & 0 deletions src/Mimic.UnitTests/Fixtures/InvocationFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ internal sealed class InvocationFixture : Invocation
{
private static readonly MethodInfo DefaultMethod = typeof(InvocationFixture).GetMethod("ToString")!;

public bool ProceededToBase { get; private set; }

public InvocationFixture(MethodInfo? method = null)
: base(typeof(InvocationFixture), method ?? DefaultMethod, Array.Empty<object?>())
{
Expand All @@ -22,6 +24,8 @@ public InvocationFixture(Type proxyType, MethodInfo? method, object?[] arguments
{
}

public override object Proceed() => ProceededToBase = true;

public static InvocationFixture ForMethod<T>(string name, object?[]? arguments = null)
{
var method = typeof(T).GetMethod(name);
Expand Down
39 changes: 39 additions & 0 deletions src/Mimic.UnitTests/Proxy/ProxyGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,40 @@ public void MarkVerified_WithSetupPredicate_AndMarkMatchedByWasPreviouslyCalledW
_interceptor.Intercepted.ShouldBeTrue();
}

[Fact]
public void Proceed_WhenUnderlyingInvocationHasBeenDetached_ShouldThrowAssertionException()
{
Invocation? savedInvocation = null;
_interceptor.Callback = invocation =>
{
savedInvocation = invocation;
};

_proxyObject.ReturnsVoid();
_interceptor.Intercepted.ShouldBeTrue();

savedInvocation.ShouldNotBeNull();

var ex = Should.Throw<Guard.AssertionException>(() => savedInvocation.Proceed());
ex.Message.ShouldContain("_underlyingInvocation must not be null");
}

[Theory]
[AutoData]
public void Proceed_ShouldReturnValueReturnedByBaseImplementation(string sValue, int iValue)
{
var interceptor = new InterceptorFixture();
var result = (C)ProxyGenerator.Instance.GenerateProxy(typeof(C), Type.EmptyTypes, [sValue, iValue], interceptor);

interceptor.Callback = invocation =>
{
invocation.Proceed().ShouldBe($"Result from base: {sValue}");
};

result.VirtualMethod();
interceptor.Intercepted.ShouldBeTrue();
}

[Fact]
public void ToString_WithGetter_ShouldReturnOnlyPropertyName()
{
Expand Down Expand Up @@ -389,8 +423,13 @@ public enum E { None = 0, One = 1, Two = 2 }

public abstract class C
{
private readonly string _sValue;

public C(string sValue, int iValue)
{
_sValue = sValue;
}

public virtual string VirtualMethod() => $"Result from base: {_sValue}";
}
}
27 changes: 25 additions & 2 deletions src/Mimic.UnitTests/Setup/Fluent/SequenceSetupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,18 @@ public void Throws_WithThreeParameterFunc_ShouldCorrectlySetSequenceBehaviour(in

#endregion

[Fact]
public void Proceed_ShouldCorrectlySetProceedBehaviour()
{
var methodCallSetup = ToMethodCallSetup<AbstractSubject>(m => m.VirtualMethod());
var setup = new SequenceSetup(methodCallSetup);

setup.Proceed().ShouldBeSameAs(setup);

(methodCallSetup.ConfiguredBehaviours.ReturnOrThrow as SequenceBehaviour).ShouldNotBeNull();
(methodCallSetup.ConfiguredBehaviours.ReturnOrThrow as SequenceBehaviour)!.Remaining.ShouldBe(1);
}

[Fact]
public void Next_ShouldCorrectlySetSequenceBehaviour()
{
Expand All @@ -323,9 +335,12 @@ public void Next_ShouldCorrectlySetSequenceBehaviour()
(methodCallSetup.ConfiguredBehaviours.ReturnOrThrow as SequenceBehaviour)!.Remaining.ShouldBe(1);
}

private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> expression)
private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> expression) => ToMethodCallSetup<ISubject>(expression);

private static MethodCallSetup ToMethodCallSetup<T>(Expression<Action<T>> expression)
where T : class
{
var mimic = new Mimic<ISubject>();
var mimic = new Mimic<T>();
var methodCallExpression = (MethodCallExpression)expression.Body;
var methodExpectation = new MethodExpectation(expression, methodCallExpression.Method, methodCallExpression.Arguments);

Expand Down Expand Up @@ -353,4 +368,12 @@ internal interface ISubject
public void MethodWithParameters(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15);
public void MethodWithParameters(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15, int v16);
}

// ReSharper disable once MemberCanBePrivate.Global
internal abstract class AbstractSubject
{
public virtual void VirtualMethod()
{
}
}
}
25 changes: 23 additions & 2 deletions src/Mimic.UnitTests/Setup/Fluent/SequenceSetupTests`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,9 +562,24 @@ public void Returns_WithThreeParameterFunc_ShouldCorrectlySetSequenceBehaviour(i

#endregion

private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> expression)
[Fact]
public void Proceed_ShouldCorrectlySetProceedBehaviour()
{
var methodCallSetup = ToMethodCallSetup<AbstractSubject>(m => m.VirtualMethod());
var setup = new SequenceSetup<string>(methodCallSetup);

setup.Proceed().ShouldBeSameAs(setup);

(methodCallSetup.ConfiguredBehaviours.ReturnOrThrow as SequenceBehaviour).ShouldNotBeNull();
(methodCallSetup.ConfiguredBehaviours.ReturnOrThrow as SequenceBehaviour)!.Remaining.ShouldBe(1);
}

private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> expression) => ToMethodCallSetup<ISubject>(expression);

private static MethodCallSetup ToMethodCallSetup<T>(Expression<Action<T>> expression)
where T : class
{
var mimic = new Mimic<ISubject>();
var mimic = new Mimic<T>();
var methodCallExpression = (MethodCallExpression)expression.Body;
var methodExpectation = new MethodExpectation(expression, methodCallExpression.Method, methodCallExpression.Arguments);

Expand Down Expand Up @@ -592,4 +607,10 @@ internal interface ISubject
public string MethodWithParameters(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15);
public string MethodWithParameters(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15, int v16);
}

// ReSharper disable once MemberCanBePrivate.Global
internal abstract class AbstractSubject
{
public virtual string VirtualMethod() => default!;
}
}
27 changes: 25 additions & 2 deletions src/Mimic.UnitTests/Setup/Fluent/SetupTests`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,18 @@ public void Limit_ShouldCorrectlySetExecutionLimitBehaviour(int executionLimit)
methodCallSetup.ConfiguredBehaviours.ExecutionLimit.ShouldNotBeNull();
}

[Fact]
public void Proceed_ShouldReturnInitializedSequenceSetup()
{
var methodCallSetup = ToMethodCallSetup<AbstractSubject>(m => m.VirtualMethod());
var setup = new Setup<ISubject>(methodCallSetup);

setup.Proceed().ShouldBeSameAs(setup);

methodCallSetup.ConfiguredBehaviours.ReturnOrThrow.ShouldNotBeNull();
methodCallSetup.ConfiguredBehaviours.ReturnOrThrow.ShouldBeSameAs(ProceedBehaviour.Instance);
}

[Fact]
public void AsSequence_ShouldReturnInitializedSequenceSetup()
{
Expand All @@ -548,9 +560,12 @@ public void AsSequence_ShouldReturnInitializedSequenceSetup()
methodCallSetup.ConfiguredBehaviours.ReturnOrThrow.ShouldBeNull();
}

private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> expression)
private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> expression) => ToMethodCallSetup<ISubject>(expression);

private static MethodCallSetup ToMethodCallSetup<T>(Expression<Action<T>> expression)
where T : class
{
var mimic = new Mimic<ISubject>();
var mimic = new Mimic<T>();
var methodCallExpression = (MethodCallExpression)expression.Body;
var methodExpectation = new MethodExpectation(expression, methodCallExpression.Method, methodCallExpression.Arguments);

Expand Down Expand Up @@ -578,5 +593,13 @@ internal interface ISubject
public void MethodWithParameters(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15);
public void MethodWithParameters(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15, int v16);
}

// ReSharper disable once MemberCanBePrivate.Global
internal abstract class AbstractSubject
{
public virtual void VirtualMethod()
{
}
}
}
}
25 changes: 23 additions & 2 deletions src/Mimic.UnitTests/Setup/Fluent/SetupTests`2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,18 @@ public void Returns_WithThreeParameterFunc_ShouldCorrectlySetReturnComputedValue

#endregion

[Fact]
public void Proceed_ShouldReturnInitializedSequenceSetup()
{
var methodCallSetup = ToMethodCallSetup<AbstractSubject>(m => m.VirtualMethod());
var setup = new Setup<ISubject, string>(methodCallSetup);

setup.Proceed().ShouldBeSameAs(setup);

methodCallSetup.ConfiguredBehaviours.ReturnOrThrow.ShouldNotBeNull();
methodCallSetup.ConfiguredBehaviours.ReturnOrThrow.ShouldBeSameAs(ProceedBehaviour.Instance);
}

[Fact]
public void AsSequence_ShouldReturnInitializedSequenceSetup()
{
Expand All @@ -792,9 +804,12 @@ public void AsSequence_ShouldReturnInitializedSequenceSetup()
methodCallSetup.ConfiguredBehaviours.ReturnOrThrow.ShouldBeNull();
}

private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> expression)
private static MethodCallSetup ToMethodCallSetup(Expression<Action<ISubject>> expression) => ToMethodCallSetup<ISubject>(expression);

private static MethodCallSetup ToMethodCallSetup<T>(Expression<Action<T>> expression)
where T : class
{
var mimic = new Mimic<ISubject>();
var mimic = new Mimic<T>();
var methodCallExpression = (MethodCallExpression)expression.Body;
var methodExpectation = new MethodExpectation(expression, methodCallExpression.Method, methodCallExpression.Arguments);

Expand Down Expand Up @@ -822,5 +837,11 @@ internal interface ISubject
public string MethodWithParameters(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15);
public string MethodWithParameters(int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9, int v10, int v11, int v12, int v13, int v14, int v15, int v16);
}

// ReSharper disable once MemberCanBePrivate.Global
internal abstract class AbstractSubject
{
public virtual string VirtualMethod() => default!;
}
}
}

0 comments on commit 8d3cc63

Please sign in to comment.