Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "set" methods to ProtectedAsMock #1165

Merged
merged 7 commits into from Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Moq/Mock`1.cs
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
@@ -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
stakx marked this conversation as resolved.
Show resolved Hide resolved
stakx marked this conversation as resolved.
Show resolved Hide resolved
{
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
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);
stakx marked this conversation as resolved.
Show resolved Hide resolved
/// </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.
stakx marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <param name="times">The number of times a method is expected to be called. Defaults to Times.AtLeastOnce</param>
stakx marked this conversation as resolved.
Show resolved Hide resolved
/// <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
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);
}
stakx marked this conversation as resolved.
Show resolved Hide resolved
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.
}
}

stakx marked this conversation as resolved.
Show resolved Hide resolved
}
}
109 changes: 109 additions & 0 deletions tests/Moq.Tests/ProtectedAsMockFixture.cs
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));

stakx marked this conversation as resolved.
Show resolved Hide resolved
}

[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);

stakx marked this conversation as resolved.
Show resolved Hide resolved
}

[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
stakx marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}
}
}