Skip to content

Commit

Permalink
Add "set" methods to ProtectedAsMock
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyhallett committed May 29, 2021
1 parent a6fde8b commit c7df19e
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/Moq/Mock`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ public ISetup<T> Setup(Expression<Action<T>> expression)
/// Specifies a setup on the mocked type for a call to a property setter.
/// </summary>
/// <param name="setterExpression">The Lambda expression that sets a property to a value.</param>
/// <typeparam name="TProperty">Type of the property. Typically omitted as it can be inferred from the expression.</typeparam>
/// <typeparam name="TProperty">Type of the property.</typeparam>
/// <remarks>
/// If more than one setup is set for the same property setter,
/// the latest one wins and is the one that will be executed.
Expand Down
52 changes: 52 additions & 0 deletions src/Moq/Protected/DuckSetterReplacer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Moq.Protected
{
internal sealed class DuckSetterReplacer<TMock, TAnalog> : ExpressionVisitor
{
private ParameterExpression parameterToReplace;
private ParameterExpression mockParameter;
private Type mockType = typeof(TMock);

private PropertyInfo GetMockProperty(PropertyInfo property)
{
return mockType.GetProperty(
property.Name,
BindingFlags.NonPublic | BindingFlags.Instance,
null,
property.PropertyType,
property.GetIndexParameters().Select(p => p.ParameterType).ToArray(),
new ParameterModifier[] { }
);
}

protected override Expression VisitIndex(IndexExpression node)
{
if (node.Object is ParameterExpression parameterExpression && parameterExpression == parameterToReplace)
{
return Expression.MakeIndex(mockParameter, GetMockProperty(node.Indexer), node.Arguments);
}
return base.VisitIndex(node);
}

protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression is ParameterExpression parameterExpression && parameterExpression == parameterToReplace)
{
return Expression.MakeMemberAccess(mockParameter, GetMockProperty(node.Member as PropertyInfo));
}
return base.VisitMember(node);
}

public Expression<Action<TMock>> Replace(Expression<Action<TAnalog>> expression)
{
parameterToReplace = expression.Parameters[0];
mockParameter = Expression.Parameter(typeof(TMock), parameterToReplace.Name);
return Expression.Lambda<Action<TMock>>(expression.Body.Apply(this), mockParameter);
}

}
}
46 changes: 46 additions & 0 deletions src/Moq/Protected/IProtectedAsMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,41 @@ public interface IProtectedAsMock<T, TAnalog> : IFluentInterface
/// <seealso cref="Mock{T}.Setup{TResult}(Expression{Func{T, TResult}})"/>
ISetup<T, TResult> Setup<TResult>(Expression<Func<TAnalog, TResult>> expression);


/// <summary>
/// Specifies a setup on the mocked type for a call to a property setter.
/// </summary>
/// <param name="setterExpression">The Lambda expression that sets a property to a value.</param>
/// <typeparam name="TProperty">Type of the property.</typeparam>
/// <remarks>
/// If more than one setup is set for the same property setter,
/// the latest one wins and is the one that will be executed.
/// <para>
/// This overloads allows the use of a callback already typed for the property type.
/// </para>
/// </remarks>
/// <example group="setups">
/// <code>
/// mock.SetupSet(x => x.Suspended = true);
/// </code>
/// </example>
ISetupSetter<T, TProperty> SetupSet<TProperty>(Action<TAnalog> setterExpression);

/// <summary>
/// Specifies a setup on the mocked type for a call to a property setter.
/// </summary>
/// <param name="setterExpression">Lambda expression that sets a property to a value.</param>
/// <remarks>
/// If more than one setup is set for the same property setter,
/// the latest one wins and is the one that will be executed.
/// </remarks>
/// <example group="setups">
/// <code>
/// mock.SetupSet(x => x.Suspended = true);
/// </code>
/// </example>
ISetup<T> SetupSet(Action<TAnalog> setterExpression);

/// <summary>
/// Specifies a setup on the mocked type for a call to a property getter.
/// </summary>
Expand Down Expand Up @@ -96,6 +131,17 @@ public interface IProtectedAsMock<T, TAnalog> : IFluentInterface
/// <exception cref="MockException">The specified invocation did not occur (or did not occur the specified number of times).</exception>
void Verify<TResult>(Expression<Func<TAnalog, TResult>> expression, Times? times = null, string failMessage = null);

/// <summary>
/// Verifies that a property was set on the mock, specifying a failure message.
/// </summary>
/// <param name="times">The number of times a method is expected to be called. Defaults to Times.AtLeastOnce</param>
/// <param name="setterExpression">Expression to verify.</param>
/// <param name="failMessage">Message to show if verification fails.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
void VerifySet(Action<TAnalog> setterExpression, Times? times = null, string failMessage = null);

/// <summary>
/// Verifies that a property was read on the mock.
/// </summary>
Expand Down
26 changes: 26 additions & 0 deletions src/Moq/Protected/ProtectedAsMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal sealed class ProtectedAsMock<T, TAnalog> : IProtectedAsMock<T, TAnalog>
private Mock<T> mock;

private static DuckReplacer DuckReplacerInstance = new DuckReplacer(typeof(TAnalog), typeof(T));
private static DuckSetterReplacer<T, TAnalog> DuckSetterReplacerInstance = new DuckSetterReplacer<T, TAnalog>();

public ProtectedAsMock(Mock<T> mock)
{
Expand Down Expand Up @@ -64,6 +65,18 @@ public ISetup<T> Setup(Expression<Action<TAnalog>> expression)
return new NonVoidSetupPhrase<T, TResult>(setup);
}

public ISetupSetter<T, TProperty> SetupSet<TProperty>(Action<TAnalog> setterExpression)
{
var setup = Mock.SetupSet(mock, ReplaceDuckSetter(setterExpression), condition: null);
return new SetterSetupPhrase<T, TProperty>(setup);
}

public ISetup<T> SetupSet(Action<TAnalog> setterExpression)
{
var setup = Mock.SetupSet(mock, ReplaceDuckSetter(setterExpression), condition: null);
return new VoidSetupPhrase<T>(setup);
}

public ISetupGetter<T, TProperty> SetupGet<TProperty>(Expression<Func<TAnalog, TProperty>> expression)
{
Guard.NotNull(expression, nameof(expression));
Expand Down Expand Up @@ -169,6 +182,11 @@ public void Verify<TResult>(Expression<Func<TAnalog, TResult>> expression, Times
Mock.Verify(this.mock, rewrittenExpression, times ?? Times.AtLeastOnce(), failMessage);
}

public void VerifySet(Action<TAnalog> setterExpression, Times? times = null, string failMessage = null)
{
Mock.VerifySet(mock, ReplaceDuckSetter(setterExpression), times.HasValue ? times.Value : Times.AtLeastOnce(), failMessage);
}

public void VerifyGet<TProperty>(Expression<Func<TAnalog, TProperty>> expression, Times? times = null, string failMessage = null)
{
Guard.NotNull(expression, nameof(expression));
Expand All @@ -186,6 +204,13 @@ public void VerifyGet<TProperty>(Expression<Func<TAnalog, TProperty>> expression
Mock.VerifyGet(this.mock, rewrittenExpression, times ?? Times.AtLeastOnce(), failMessage);
}

private Expression<Action<T>> ReplaceDuckSetter(Action<TAnalog> setterExpression)
{
Guard.NotNull(setterExpression, nameof(setterExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(setterExpression, mock.ConstructorArguments);
return DuckSetterReplacerInstance.Replace(expression);
}
private static LambdaExpression ReplaceDuck(LambdaExpression expression)
{
Debug.Assert(expression.Parameters.Count == 1);
Expand Down Expand Up @@ -363,5 +388,6 @@ private static bool IsCorrespondingProperty(PropertyInfo duckProperty, PropertyI
// TODO: parameter lists should be compared, too, to properly support indexers.
}
}

}
}
109 changes: 109 additions & 0 deletions tests/Moq.Tests/ProtectedAsMockFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,89 @@ public void SetupSequence_can_setup_actions()
Assert.IsType<InvalidOperationException>(exception);
}

[Fact]
public void SetUpSet_should_setup_setters()
{
this.protectedMock.SetupSet(fish => fish.ReadWritePropertyImpl = 999).Throws(ExpectedException.Instance);

mock.Object.ReadWriteProperty = 123;

Assert.Throws<ExpectedException>(() => mock.Object.ReadWriteProperty = 999);
}

[Fact]
public void SetUpSet_should_setup_setters_with_property_type()
{
int value = 0;
this.protectedMock.SetupSet<int>(fish => fish.ReadWritePropertyImpl = 999).Callback(i => value = i);

mock.Object.ReadWriteProperty = 123;
Assert.Equal(0, value);

mock.Object.ReadWriteProperty = 999;
Assert.Equal(999, value);
}

[Fact]
public void SetUpSet_should_work_recursively()
{
this.protectedMock.SetupSet(f => f.Nested.Value = 999).Throws(ExpectedException.Instance);

mock.Object.GetNested().Value = 1;

Assert.Throws<ExpectedException>(() => mock.Object.GetNested().Value = 999);
}

[Fact]
public void SetUpSet_Should_Work_With_Indexers()
{
this.protectedMock.SetupSet(
o => o[
It.IsInRange(0, 5, Range.Inclusive),
It.IsIn("Bad", "JustAsBad")
] = It.Is<int>(i => i > 10)
).Throws(ExpectedException.Instance);

mock.Object.SetMultipleIndexer(1, "Ok", 999);

Assert.Throws<ExpectedException>(() => mock.Object.SetMultipleIndexer(1, "Bad", 999));

}

[Fact]
public void VerifySet_Should_Work()
{
void VerifySet(Times? times = null,string failMessage = null)
{
this.protectedMock.VerifySet(
o => o[
It.IsInRange(0, 5, Moq.Range.Inclusive),
It.IsIn("Bad", "JustAsBad")
] = It.Is<int>(i => i > 10),
times,
failMessage
);
}
VerifySet(Times.Never());

mock.Object.SetMultipleIndexer(1, "Ok", 1);
VerifySet(Times.Never());

Assert.Throws<MockException>(() => VerifySet()); // AtLeastOnce

mock.Object.SetMultipleIndexer(1, "Bad", 999);
VerifySet(); // AtLeastOnce

mock.Object.SetMultipleIndexer(1, "JustAsBad", 12);
VerifySet(Times.Exactly(2));

Assert.Throws<MockException>(() => VerifySet(Times.AtMostOnce()));

var mockException = Assert.Throws<MockException>(() => VerifySet(Times.AtMostOnce(),"custom fail message"));
Assert.StartsWith("custom fail message", mockException.Message);

}

[Fact]
public void Verify_can_verify_method_invocations()
{
Expand Down Expand Up @@ -298,6 +381,10 @@ public void VerifyGet_includes_failure_message_in_exception()
Assert.Contains("Was not queried.", exception.Message);
}

public interface INested
{
int Value { get; set; }
}
public abstract class Foo
{
protected Foo()
Expand Down Expand Up @@ -336,6 +423,20 @@ public int GetSomething()
protected abstract void DoSomethingImpl(int arg);

protected abstract int GetSomethingImpl();

protected abstract INested Nested { get; set; }

public INested GetNested()
{
return Nested;
}

protected abstract int this[int i, string s] { get; set; }

public void SetMultipleIndexer(int index, string sIndex, int value)
{
this[index, sIndex] = value;
}
}

public interface Fooish
Expand All @@ -347,6 +448,8 @@ public interface Fooish
void DoSomethingImpl(int arg);
int GetSomethingImpl();
void NonExistentMethod();
INested Nested { get; set; }
int this[int i, string s] { get; set; }
}

public abstract class MessageHandlerBase
Expand All @@ -366,5 +469,11 @@ public interface MessageHandlerBaseish
{
void HandleImpl<TMessage>(TMessage message);
}

public class ExpectedException : Exception
{
private static ExpectedException expectedException = new ExpectedException();
public static ExpectedException Instance => expectedException;
}
}
}

0 comments on commit c7df19e

Please sign in to comment.