diff --git a/CHANGELOG.md b/CHANGELOG.md index d6e082696..16ecb26fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +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, #1185) - * Issue mocking VB.NET class with overloaded property/indexer in base class (@myurashchyk, #1153) diff --git a/src/Moq/Mock`1.cs b/src/Moq/Mock`1.cs index 34dcb165b..3d25aef42 100644 --- a/src/Moq/Mock`1.cs +++ b/src/Moq/Mock`1.cs @@ -480,7 +480,7 @@ public ISetup Setup(Expression> expression) /// Specifies a setup on the mocked type for a call to a property setter. /// /// The Lambda expression that sets a property to a value. - /// Type of the property. Typically omitted as it can be inferred from the expression. + /// Type of the property. /// /// If more than one setup is set for the same property setter, /// the latest one wins and is the one that will be executed. diff --git a/src/Moq/Protected/IProtectedAsMock.cs b/src/Moq/Protected/IProtectedAsMock.cs index 9539b9a16..354351ffc 100644 --- a/src/Moq/Protected/IProtectedAsMock.cs +++ b/src/Moq/Protected/IProtectedAsMock.cs @@ -39,6 +39,41 @@ public interface IProtectedAsMock : IFluentInterface /// ISetup Setup(Expression> expression); + + /// + /// Specifies a setup on the mocked type for a call to a property setter. + /// + /// The Lambda expression that sets a property to a value. + /// Type of the property. + /// + /// If more than one setup is set for the same property setter, + /// the latest one wins and is the one that will be executed. + /// + /// This overloads allows the use of a callback already typed for the property type. + /// + /// + /// + /// + /// mock.SetupSet<bool>(x => x.Suspended = true); + /// + /// + ISetupSetter SetupSet(Action setterExpression); + + /// + /// Specifies a setup on the mocked type for a call to a property setter. + /// + /// Lambda expression that sets a property to a value. + /// + /// If more than one setup is set for the same property setter, + /// the latest one wins and is the one that will be executed. + /// + /// + /// + /// mock.SetupSet(x => x.Suspended = true); + /// + /// + ISetup SetupSet(Action setterExpression); + /// /// Specifies a setup on the mocked type for a call to a property getter. /// @@ -96,6 +131,20 @@ public interface IProtectedAsMock : IFluentInterface /// The specified invocation did not occur (or did not occur the specified number of times). void Verify(Expression> expression, Times? times = null, string failMessage = null); + /// + /// Verifies that a property was set on the mock. + /// + /// 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 . + /// + void VerifySet(Action setterExpression, Times? times = null, string failMessage = null); + /// /// Verifies that a property was read on the mock. /// diff --git a/src/Moq/Protected/ProtectedAsMock.cs b/src/Moq/Protected/ProtectedAsMock.cs index 8be67daf4..02c1a2c41 100644 --- a/src/Moq/Protected/ProtectedAsMock.cs +++ b/src/Moq/Protected/ProtectedAsMock.cs @@ -64,6 +64,24 @@ public ISetup Setup(Expression> expression) return new NonVoidSetupPhrase(setup); } + public ISetupSetter SetupSet(Action setterExpression) + { + 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) + { + Guard.NotNull(setterExpression, nameof(setterExpression)); + + var rewrittenExpression = ReconstructAndReplaceSetter(setterExpression); + var setup = Mock.SetupSet(mock, rewrittenExpression, condition: null); + return new VoidSetupPhrase(setup); + } + public ISetupGetter SetupGet(Expression> expression) { Guard.NotNull(expression, nameof(expression)); @@ -169,6 +187,14 @@ public void Verify(Expression> expression, Times Mock.Verify(this.mock, rewrittenExpression, times ?? Times.AtLeastOnce(), failMessage); } + public void VerifySet(Action setterExpression, Times? times = null, string failMessage = null) + { + 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) { Guard.NotNull(expression, nameof(expression)); @@ -186,6 +212,12 @@ public void VerifyGet(Expression> expression Mock.VerifyGet(this.mock, rewrittenExpression, times ?? Times.AtLeastOnce(), failMessage); } + private LambdaExpression ReconstructAndReplaceSetter(Action setterExpression) + { + var expression = ExpressionReconstructor.Instance.ReconstructExpression(setterExpression, mock.ConstructorArguments); + return ReplaceDuck(expression); + } + private static LambdaExpression ReplaceDuck(LambdaExpression expression) { Debug.Assert(expression.Parameters.Count == 1); @@ -221,6 +253,16 @@ protected override Expression VisitMethodCall(MethodCallExpression 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) { if (node.Expression is ParameterExpression left && left.Type == this.duckType) diff --git a/tests/Moq.Tests/ProtectedAsMockFixture.cs b/tests/Moq.Tests/ProtectedAsMockFixture.cs index dd0a7356a..8fdc97891 100644 --- a/tests/Moq.Tests/ProtectedAsMockFixture.cs +++ b/tests/Moq.Tests/ProtectedAsMockFixture.cs @@ -167,9 +167,10 @@ public void SetUpGet_can_automock() Assert.Equal(42, actual); } - [Fact] void SetupGet_can_setup_virtual_property() + [Fact] + public void SetupGet_can_setup_virtual_property() { - this.protectedMock.SetupGet(m => m.Virtual).Returns(42); + this.protectedMock.SetupGet(m => m.VirtualGet).Returns(42); var actual = mock.Object.GetVirtual(); @@ -239,6 +240,96 @@ public void SetupSequence_can_setup_actions() Assert.IsType(exception); } + [Fact] + public void SetUpSet_should_setup_setters() + { + this.protectedMock.SetupSet(fish => fish.ReadWritePropertyImpl = 999).Throws(ExpectedException.Instance); + + mock.Object.ReadWriteProperty = 123; + + Assert.Throws(() => mock.Object.ReadWriteProperty = 999); + } + + [Fact] + public void SetUpSet_should_setup_setters_with_property_type() + { + int value = 0; + this.protectedMock.SetupSet(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(() => 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(i => i > 10) + ).Throws(ExpectedException.Instance); + + 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] + 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(i => i > 10), + times, + failMessage + ); + } + VerifySet(Times.Never()); + + mock.Object.SetMultipleIndexer(1, "Ok", 1); + VerifySet(Times.Never()); + + Assert.Throws(() => VerifySet()); // AtLeastOnce + + mock.Object.SetMultipleIndexer(1, "Bad", 999); + VerifySet(); // AtLeastOnce + + mock.Object.SetMultipleIndexer(1, "JustAsBad", 12); + VerifySet(Times.Exactly(2)); + + Assert.Throws(() => VerifySet(Times.AtMostOnce())); + + var mockException = Assert.Throws(() => VerifySet(Times.AtMostOnce(),"custom fail message")); + Assert.StartsWith("custom fail message", mockException.Message); + } + [Fact] public void Verify_can_verify_method_invocations() { @@ -326,7 +417,7 @@ public void VerifyGet_includes_failure_message_in_exception() public interface INested { - int Value { get; } + int Value { get; set; } int Method(int value); } @@ -376,23 +467,49 @@ public INested GetNested() return Nested; } - private int virtualProperty; - public virtual int Virtual + protected abstract int this[int i, string s] { get; set; } + + public void SetMultipleIndexer(int index, string sIndex, int value) + { + this[index, sIndex] = value; + } + + 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 virtualProperty; + return _virtualGet; } set { - virtualProperty = value; + _virtualGet = value; } } public int GetVirtual() { - return Virtual; + return VirtualGet; } } @@ -406,7 +523,9 @@ public interface Fooish int GetSomethingImpl(); void NonExistentMethod(); INested Nested { get; set; } - int Virtual { get; set; } + int this[int i, string s] { get; set; } + int VirtualGet { get; set; } + int VirtualSet { get; set; } } public abstract class MessageHandlerBase @@ -426,5 +545,11 @@ public interface MessageHandlerBaseish { void HandleImpl(TMessage message); } + + public class ExpectedException : Exception + { + private static ExpectedException expectedException = new ExpectedException(); + public static ExpectedException Instance => expectedException; + } } }