diff --git a/CHANGELOG.md b/CHANGELOG.md index 936deb043..37b969ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1 ## Unreleased +#### Added + +* `SetupSet`, `VerifySet` methods for `mock.Protected().As<>()` (@tonyhallett, #1165) + #### Fixed +* Virtual properties and automocking not working for `mock.Protected().As<>()` (@tonyhallett, #1165) * Issue mocking VB.NET class with overloaded property/indexer in base class (@myurashchyk, #1153) diff --git a/src/Moq/Protected/DuckSetterReplacer.cs b/src/Moq/Protected/DuckSetterReplacer.cs deleted file mode 100644 index b7532e465..000000000 --- a/src/Moq/Protected/DuckSetterReplacer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Moq.Protected -{ - internal sealed class DuckSetterReplacer : 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> Replace(Expression> expression) - { - parameterToReplace = expression.Parameters[0]; - mockParameter = Expression.Parameter(typeof(TMock), parameterToReplace.Name); - return Expression.Lambda>(expression.Body.Apply(this), mockParameter); - } - - } -} diff --git a/src/Moq/Protected/IProtectedAsMock.cs b/src/Moq/Protected/IProtectedAsMock.cs index 9c8a02ba6..354351ffc 100644 --- a/src/Moq/Protected/IProtectedAsMock.cs +++ b/src/Moq/Protected/IProtectedAsMock.cs @@ -54,7 +54,7 @@ public interface IProtectedAsMock : IFluentInterface /// /// /// - /// mock.SetupSet(x => x.Suspended = true); + /// mock.SetupSet<bool>(x => x.Suspended = true); /// /// ISetupSetter SetupSet(Action setterExpression); @@ -132,10 +132,13 @@ public interface IProtectedAsMock : IFluentInterface void Verify(Expression> expression, Times? times = null, string failMessage = null); /// - /// Verifies that a property was set on the mock, specifying a failure message. + /// Verifies that a property was set on the mock. /// - /// The number of times a method is expected to be called. Defaults to Times.AtLeastOnce /// Expression to verify. + /// + /// Number of times that the setter is expected to have occurred. + /// If omitted, assumed to be . + /// /// Message to show if verification fails. /// /// The invocation was not called the number of times specified by . diff --git a/src/Moq/Protected/ProtectedAsMock.cs b/src/Moq/Protected/ProtectedAsMock.cs index 4dc9e4ad1..b82718ec9 100644 --- a/src/Moq/Protected/ProtectedAsMock.cs +++ b/src/Moq/Protected/ProtectedAsMock.cs @@ -20,7 +20,6 @@ internal sealed class ProtectedAsMock : IProtectedAsMock private Mock mock; private static DuckReplacer DuckReplacerInstance = new DuckReplacer(typeof(TAnalog), typeof(T)); - private static DuckSetterReplacer DuckSetterReplacerInstance = new DuckSetterReplacer(); public ProtectedAsMock(Mock mock) { @@ -67,13 +66,19 @@ public ISetup Setup(Expression> expression) public ISetupSetter SetupSet(Action setterExpression) { - var setup = Mock.SetupSet(mock, ReplaceDuckSetter(setterExpression), condition: null); + Guard.NotNull(setterExpression, nameof(setterExpression)); + + var rewrittenExpression = ReconstructAndReplaceSetter(setterExpression); + var setup = Mock.SetupSet(mock, rewrittenExpression, condition: null); return new SetterSetupPhrase(setup); } public ISetup SetupSet(Action setterExpression) { - var setup = Mock.SetupSet(mock, ReplaceDuckSetter(setterExpression), condition: null); + Guard.NotNull(setterExpression, nameof(setterExpression)); + + var rewrittenExpression = ReconstructAndReplaceSetter(setterExpression); + var setup = Mock.SetupSet(mock, rewrittenExpression, condition: null); return new VoidSetupPhrase(setup); } @@ -184,7 +189,10 @@ public void Verify(Expression> expression, Times public void VerifySet(Action setterExpression, Times? times = null, string failMessage = null) { - Mock.VerifySet(mock, ReplaceDuckSetter(setterExpression), times.HasValue ? times.Value : Times.AtLeastOnce(), failMessage); + Guard.NotNull(setterExpression, nameof(setterExpression)); + + var rewrittenExpression = ReconstructAndReplaceSetter(setterExpression); + Mock.VerifySet(mock, rewrittenExpression, times.HasValue ? times.Value : Times.AtLeastOnce(), failMessage); } public void VerifyGet(Expression> expression, Times? times = null, string failMessage = null) @@ -204,13 +212,12 @@ public void VerifyGet(Expression> expression Mock.VerifyGet(this.mock, rewrittenExpression, times ?? Times.AtLeastOnce(), failMessage); } - private Expression> ReplaceDuckSetter(Action setterExpression) + private LambdaExpression ReconstructAndReplaceSetter(Action setterExpression) { - Guard.NotNull(setterExpression, nameof(setterExpression)); - var expression = ExpressionReconstructor.Instance.ReconstructExpression(setterExpression, mock.ConstructorArguments); - return DuckSetterReplacerInstance.Replace(expression); + return ReplaceDuck(expression); } + private static LambdaExpression ReplaceDuck(LambdaExpression expression) { Debug.Assert(expression.Parameters.Count == 1); @@ -239,11 +246,21 @@ protected override Expression VisitMethodCall(MethodCallExpression node) { var targetParameter = Expression.Parameter(this.targetType, left.Name); return Expression.Call(targetParameter, FindCorrespondingMethod(node.Method), node.Arguments); - } + } else { - return node; + return base.VisitMethodCall(node); + } + } + + protected override Expression VisitIndex(IndexExpression node) + { + if (node.Object is ParameterExpression left && left.Type == this.duckType) + { + var targetParameter = Expression.Parameter(this.targetType, left.Name); + return Expression.MakeIndex(targetParameter, FindCorrespondingProperty(node.Indexer), node.Arguments); } + return base.VisitIndex(node); } protected override Expression VisitMember(MemberExpression node) @@ -255,7 +272,7 @@ protected override Expression VisitMember(MemberExpression node) } else { - return node; + return base.VisitMember(node); } } @@ -305,7 +322,7 @@ private PropertyInfo FindCorrespondingProperty(PropertyInfo duckProperty) { var candidateTargetProperties = this.targetType - .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) + .GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) .Where(ctp => IsCorrespondingProperty(duckProperty, ctp)) .ToArray(); @@ -388,6 +405,5 @@ private static bool IsCorrespondingProperty(PropertyInfo duckProperty, PropertyI // TODO: parameter lists should be compared, too, to properly support indexers. } } - } } diff --git a/tests/Moq.Tests/ProtectedAsMockFixture.cs b/tests/Moq.Tests/ProtectedAsMockFixture.cs index ef518ae67..21da0d2ba 100644 --- a/tests/Moq.Tests/ProtectedAsMockFixture.cs +++ b/tests/Moq.Tests/ProtectedAsMockFixture.cs @@ -130,6 +130,13 @@ public void Setup_can_setup_generic_method() Assert.Equal(3, handledMessages.Count); } + [Fact] + public void Setup_can_automock() + { + this.protectedMock.Setup(m => m.Nested.Method(1)).Returns(123); + Assert.Equal(123, mock.Object.GetNested().Method(1)); + } + [Fact] public void SetupGet_can_setup_readonly_property() { @@ -150,6 +157,26 @@ public void SetupGet_can_setup_readwrite_property() Assert.Equal(42, actual); } + [Fact] + public void SetUpGet_can_automock() + { + this.protectedMock.SetupGet(m => m.Nested.Value).Returns(42); + + var actual = mock.Object.GetNested().Value; + + Assert.Equal(42, actual); + } + + [Fact] + public void SetupGet_can_setup_virtual_property() + { + this.protectedMock.SetupGet(m => m.VirtualGet).Returns(42); + + var actual = mock.Object.GetVirtual(); + + Assert.Equal(42, actual); + } + [Fact] public void SetupProperty_can_setup_readwrite_property() { @@ -259,7 +286,15 @@ public void SetUpSet_Should_Work_With_Indexers() mock.Object.SetMultipleIndexer(1, "Ok", 999); Assert.Throws(() => mock.Object.SetMultipleIndexer(1, "Bad", 999)); + } + + [Fact] + public void SetupSet_can_setup_virtual_property() + { + this.protectedMock.SetupSet(m => m.VirtualSet = 999).Throws(new ExpectedException()); + mock.Object.SetVirtual(123); + Assert.Throws(() => mock.Object.SetVirtual(999)); } [Fact] @@ -293,7 +328,6 @@ void VerifySet(Times? times = null,string failMessage = null) var mockException = Assert.Throws(() => VerifySet(Times.AtMostOnce(),"custom fail message")); Assert.StartsWith("custom fail message", mockException.Message); - } [Fact] @@ -384,12 +418,51 @@ public void VerifyGet_includes_failure_message_in_exception() public interface INested { int Value { get; set; } + int Method(int value); } + public abstract class Foo { protected Foo() { } + private int _virtualSet; + public virtual int VirtualSet + { + get + { + return _virtualSet; + } + protected set + { + _virtualSet = value; + } + + } + + public void SetVirtual(int value) + { + VirtualSet = value; + } + + private int _virtualGet; + public virtual int VirtualGet + { + protected get + { + return _virtualGet; + } + set + { + _virtualGet = value; + } + + } + + public int GetVirtual() + { + return VirtualGet; + } public int ReadOnlyProperty => this.ReadOnlyPropertyImpl; @@ -437,10 +510,17 @@ public void SetMultipleIndexer(int index, string sIndex, int value) { this[index, sIndex] = value; } + + public int GetMultipleIndexer(int index, string sIndex) + { + return this[index, sIndex]; + } } public interface Fooish { + int VirtualGet { get; set; } + int VirtualSet { get; set; } int ReadOnlyPropertyImpl { get; } int ReadWritePropertyImpl { get; set; } int NonExistentProperty { get; }