diff --git a/src/Moq/Moq.Sdk.Tests/DefaultMockTests.cs b/src/Moq/Moq.Sdk.Tests/DefaultMockTests.cs index af4cd205..42bfbe56 100644 --- a/src/Moq/Moq.Sdk.Tests/DefaultMockTests.cs +++ b/src/Moq/Moq.Sdk.Tests/DefaultMockTests.cs @@ -14,19 +14,35 @@ public void ThrowsIfNullStunt() => Assert.Throws(() => new DefaultMock(null)); [Fact] - public void AddsMockTrackingBehavior() + public void AddsMockContextBehavior() { var mock = new DefaultMock(new FakeStunt()); - Assert.Collection(mock.Behaviors, x => Assert.IsType(x)); + Assert.Contains(mock.Behaviors, x => x is MockContextBehavior); } [Fact] - public void PreventsDuplicateMockTrackingBehavior() + public void AddsMockRecordingBehavior() { var mock = new DefaultMock(new FakeStunt()); - Assert.Throws(() => mock.Behaviors.Add(new MockTrackingBehavior())); + Assert.Contains(mock.Behaviors, x => x is MockRecordingBehavior); + } + + [Fact] + public void PreventsDuplicateMockContextBehavior() + { + var mock = new DefaultMock(new FakeStunt()); + + Assert.Throws(() => mock.Behaviors.Add(new MockContextBehavior())); + } + + [Fact] + public void PreventsDuplicateMockRecordingBehavior() + { + var mock = new DefaultMock(new FakeStunt()); + + Assert.Throws(() => mock.Behaviors.Add(new MockRecordingBehavior())); } [Fact] @@ -40,18 +56,19 @@ public void TrackMockBehaviors() new MethodInvocation(stunt, typeof(FakeStunt).GetMethod("Do")), Array.Empty()); + var initialBehaviors = stunt.Behaviors.Count; var behavior = new MockBehaviorPipeline(setup); stunt.AddBehavior(behavior); stunt.AddBehavior(new DelegateStuntBehavior((m, n) => n().Invoke(m, n))); - Assert.Equal(3, stunt.Behaviors.Count); + Assert.Equal(initialBehaviors + 2, stunt.Behaviors.Count); Assert.Single(stunt.Mock.Setups); Assert.Same(behavior, stunt.Mock.GetPipeline(setup)); stunt.Behaviors.Remove(behavior); - Assert.Equal(2, stunt.Behaviors.Count); + Assert.Equal(initialBehaviors + 1, stunt.Behaviors.Count); Assert.Empty(stunt.Mock.Setups); } @@ -59,6 +76,10 @@ public void TrackMockBehaviors() public void AddPipelineForSetupIfMissing() { var stunt = new FakeStunt(); + // Forces initialization of the default mock. + Assert.NotNull(stunt.Mock); + + var initialBehaviors = stunt.Behaviors.Count; var setup = new MockSetup( new MethodInvocation(stunt, typeof(FakeStunt).GetMethod("Do")), Array.Empty()); @@ -66,7 +87,7 @@ public void AddPipelineForSetupIfMissing() var behavior = stunt.Mock.GetPipeline(setup); Assert.NotNull(behavior); - Assert.Equal(2, stunt.Behaviors.Count); + Assert.Equal(initialBehaviors + 1, stunt.Behaviors.Count); Assert.Single(stunt.Mock.Setups); } diff --git a/src/Moq/Moq.Sdk.Tests/Fakes.cs b/src/Moq/Moq.Sdk.Tests/Fakes.cs index 2ed47df3..f0481445 100644 --- a/src/Moq/Moq.Sdk.Tests/Fakes.cs +++ b/src/Moq/Moq.Sdk.Tests/Fakes.cs @@ -28,6 +28,10 @@ public class FakeSetup : IMockSetup public IArgumentMatcher[] Matchers { get; set; } = new IArgumentMatcher[0]; + public Times? Occurrence { get; set; } + + public StateBag State { get; } = new StateBag(); + IMethodInvocation IMockSetup.Invocation => Invocation; public bool Equals(IMockSetup other) => base.Equals(other); diff --git a/src/Moq/Moq.Sdk.Tests/MockBehaviorTests.cs b/src/Moq/Moq.Sdk.Tests/MockBehaviorTests.cs index 037c1fd8..321b4853 100644 --- a/src/Moq/Moq.Sdk.Tests/MockBehaviorTests.cs +++ b/src/Moq/Moq.Sdk.Tests/MockBehaviorTests.cs @@ -34,7 +34,7 @@ public void ExecutesAnonymousBehavior() [Fact] public void RecordsInvocation() { - var behavior = new MockTrackingBehavior(); + var behavior = new MockRecordingBehavior(); var mock = new Mocked(); behavior.Execute(new MethodInvocation(mock, typeof(object).GetMethod(nameof(object.ToString))), @@ -46,12 +46,12 @@ public void RecordsInvocation() [Fact] public void ThrowsForNonIMocked() { - var behavior = new MockTrackingBehavior(); + var behavior = new MockRecordingBehavior(); Assert.Throws(() => behavior.Execute(new MethodInvocation( new object(), typeof(Mocked).GetProperty(nameof(IMocked.Mock)).GetGetMethod()), - null)); + () => (m, n) => m.CreateValueReturn(null))); } [Fact] diff --git a/src/Moq/Moq.Sdk.Tests/MockTrackingBehaviorTests.cs b/src/Moq/Moq.Sdk.Tests/MockTrackingBehaviorTests.cs index 23d29b47..5dc0372e 100644 --- a/src/Moq/Moq.Sdk.Tests/MockTrackingBehaviorTests.cs +++ b/src/Moq/Moq.Sdk.Tests/MockTrackingBehaviorTests.cs @@ -13,7 +13,7 @@ public void SetsCurrentInvocationAndSetup() { var target = new TrackingMock(); var invocation = new MethodInvocation(target, typeof(TrackingMock).GetMethod(nameof(TrackingMock.Do))); - var tracking = new MockTrackingBehavior(); + var tracking = new MockContextBehavior(); Assert.NotNull(tracking.Execute(invocation, () => (m, n) => m.CreateValueReturn(null))); @@ -27,9 +27,9 @@ public void RecordsInvocation() { var target = new TrackingMock(); var invocation = new MethodInvocation(target, typeof(TrackingMock).GetMethod(nameof(TrackingMock.Do))); - var tracking = new MockTrackingBehavior(); + var recording = new MockRecordingBehavior(); - Assert.NotNull(tracking.Execute(invocation, () => (m, n) => m.CreateValueReturn(null))); + Assert.NotNull(recording.Execute(invocation, () => (m, n) => m.CreateValueReturn(null))); Assert.Single(target.Mock.Invocations); } @@ -39,7 +39,7 @@ public void SkipInvocationRecordingIfSetupScopeActive() { var target = new TrackingMock(); var invocation = new MethodInvocation(target, typeof(TrackingMock).GetMethod(nameof(TrackingMock.Do))); - var tracking = new MockTrackingBehavior(); + var tracking = new MockContextBehavior(); using (new SetupScope()) { diff --git a/src/Moq/Moq.Sdk.Tests/TimesFixture.cs b/src/Moq/Moq.Sdk.Tests/TimesFixture.cs new file mode 100644 index 00000000..affc3e46 --- /dev/null +++ b/src/Moq/Moq.Sdk.Tests/TimesFixture.cs @@ -0,0 +1,215 @@ +using System; +using Xunit; + +namespace Moq.Sdk.Tests +{ + public class TimesFixture + { + [Theory] + [InlineData(0, false)] + [InlineData(1, true)] + [InlineData(int.MaxValue, true)] + public void DefaultTimesRangesBetweenOneAndMaxValue(int count, bool verifies) + { + Assert.Equal(verifies, default(Times).Validate(count)); + } + + [Fact] + public void AtLeastOnceRangesBetweenOneAndMaxValue() + { + var target = Times.AtLeastOnce; + + Assert.False(target.Validate(-1)); + Assert.False(target.Validate(0)); + Assert.True(target.Validate(1)); + Assert.True(target.Validate(5)); + Assert.True(target.Validate(int.MaxValue)); + } + + [Fact] + public void AtLeastThrowsIfTimesLessThanOne() + { + Assert.Throws(() => Times.AtLeast(0)); + Assert.Throws(() => Times.AtLeast(-1)); + } + + [Fact] + public void AtLeastRangesBetweenTimesAndMaxValue() + { + var target = Times.AtLeast(10); + + Assert.False(target.Validate(-1)); + Assert.False(target.Validate(0)); + Assert.False(target.Validate(9)); + Assert.True(target.Validate(10)); + Assert.True(target.Validate(int.MaxValue)); + } + + [Fact] + public void AtMostOnceRangesBetweenZeroAndOne() + { + var target = Times.AtMostOnce; + + Assert.False(target.Validate(-1)); + Assert.True(target.Validate(0)); + Assert.True(target.Validate(1)); + Assert.False(target.Validate(5)); + Assert.False(target.Validate(int.MaxValue)); + } + + [Fact] + public void AtMostThrowsIfTimesLessThanZero() + { + Assert.Throws(() => Times.AtMost(-1)); + Assert.Throws(() => Times.AtMost(-2)); + } + + [Fact] + public void AtMostRangesBetweenZeroAndTimes() + { + var target = Times.AtMost(10); + + Assert.False(target.Validate(-1)); + Assert.True(target.Validate(0)); + Assert.True(target.Validate(6)); + Assert.True(target.Validate(10)); + Assert.False(target.Validate(11)); + Assert.False(target.Validate(int.MaxValue)); + } + + [Fact] + public void ExactlyThrowsIfTimesLessThanZero() + { + Assert.Throws(() => Times.Exactly(-1)); + Assert.Throws(() => Times.Exactly(-2)); + } + + [Fact] + public void ExactlyCheckExactTimes() + { + var target = Times.Exactly(10); + + Assert.False(target.Validate(-1)); + Assert.False(target.Validate(0)); + Assert.False(target.Validate(9)); + Assert.True(target.Validate(10)); + Assert.False(target.Validate(11)); + Assert.False(target.Validate(int.MaxValue)); + } + + [Fact] + public void NeverChecksZeroTimes() + { + var target = Times.Never; + + Assert.False(target.Validate(-1)); + Assert.True(target.Validate(0)); + Assert.False(target.Validate(1)); + Assert.False(target.Validate(int.MaxValue)); + } + + [Fact] + public void OnceChecksOneTime() + { + var target = Times.Once; + + Assert.False(target.Validate(-1)); + Assert.False(target.Validate(0)); + Assert.True(target.Validate(1)); + Assert.False(target.Validate(int.MaxValue)); + } + + public class Deconstruction + { + [Fact] + public void AtLeast_n_deconstructs_to_n_MaxValue() + { + const int n = 42; + + var (from, to) = Times.AtLeast(n); + Assert.Equal(n, from); + Assert.Equal(int.MaxValue, to); + } + + [Fact] + public void AtLeastOnce_deconstructs_to_1_MaxValue() + { + var (from, to) = Times.AtLeastOnce; + Assert.Equal(1, from); + Assert.Equal(int.MaxValue, to); + } + + [Fact] + public void AtMost_n_deconstructs_to_0_n() + { + const int n = 42; + + var (from, to) = Times.AtMost(n); + Assert.Equal(0, from); + Assert.Equal(n, to); + } + + [Fact] + public void AtMostOnce_deconstructs_to_0_1() + { + var (from, to) = Times.AtMostOnce; + Assert.Equal(0, from); + Assert.Equal(1, to); + } + + [Fact] + public void Exactly_n_deconstructs_to_n_n() + { + const int n = 42; + var (from, to) = Times.Exactly(n); + Assert.Equal(n, from); + Assert.Equal(n, to); + } + + [Fact] + public void Once_deconstructs_to_1_1() + { + var (from, to) = Times.Once; + Assert.Equal(1, from); + Assert.Equal(1, to); + } + + [Fact] + public void Never_deconstructs_to_0_0() + { + var (from, to) = Times.Never; + Assert.Equal(0, from); + Assert.Equal(0, to); + } + } + + public class Equality + { +#pragma warning disable xUnit2000 // Constants and literals should be the expected argument + [Fact] + public void default_Equals_AtLeastOnce() + { + Assert.Equal(Times.AtLeastOnce, default(Times)); + } +#pragma warning restore xUnit2000 + + [Fact] + public void default_GetHashCode_equals_AtLeastOnce_GetHashCode() + { + Assert.Equal(Times.AtLeastOnce.GetHashCode(), default(Times).GetHashCode()); + } + + [Fact] + public void Once_equals_Once() + { + Assert.Equal(Times.Once, Times.Once); + } + + [Fact] + public void Once_equals_Exactly_1() + { + Assert.Equal(Times.Once, Times.Exactly(1)); + } + } + } +} diff --git a/src/Moq/Moq.Sdk/DefaultMock.cs b/src/Moq/Moq.Sdk/DefaultMock.cs index 452a7e92..397f6b6d 100644 --- a/src/Moq/Moq.Sdk/DefaultMock.cs +++ b/src/Moq/Moq.Sdk/DefaultMock.cs @@ -13,7 +13,7 @@ namespace Moq.Sdk /// /// Default implementation of the mock introspection API , /// which also ensures that the contains - /// the when initially created. + /// the when initially created. /// [DebuggerDisplay("Invocations = {Invocations.Count}", Name = nameof(IMocked) + "." + nameof(IMocked.Mock))] public class DefaultMock : IMock @@ -30,8 +30,11 @@ public DefaultMock(IStunt stunt) { this.stunt = stunt ?? throw new ArgumentNullException(nameof(stunt)); - if (!stunt.Behaviors.OfType().Any()) - stunt.Behaviors.Insert(0, new MockTrackingBehavior()); + if (!stunt.Behaviors.OfType().Any()) + stunt.Behaviors.Insert(0, new MockContextBehavior()); + + if (!stunt.Behaviors.OfType().Any()) + stunt.Behaviors.Insert(1, new MockRecordingBehavior()); stunt.Behaviors.CollectionChanged += OnBehaviorsChanged; } @@ -40,14 +43,14 @@ public DefaultMock(IStunt stunt) public ObservableCollection Behaviors => stunt.Behaviors; /// - public ICollection Invocations { get; } = new HashSet(); + public ICollection Invocations { get; } = new List(); /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] public object Object => stunt; /// - public MockState State { get; } = new MockState(); + public StateBag State { get; set; } = new StateBag(); /// public IEnumerable Setups => setupBehaviorMap.Values; @@ -58,10 +61,16 @@ public IMockBehaviorPipeline GetPipeline(IMockSetup setup) { var behavior = new MockBehaviorPipeline(x); // The tracking behavior must appear before the mock behaviors. - var tracking = Behaviors.OfType().FirstOrDefault(); + var context = Behaviors.OfType().FirstOrDefault(); + // If there is a recording behavior, it must be before mock behaviors too. + var recording = Behaviors.OfType().FirstOrDefault(); + + var index = context == null ? 0 : Behaviors.IndexOf(context); + if (recording != null) + index = Math.Max(index, Behaviors.IndexOf(recording)); + // NOTE: latest setup wins, since it goes to the top of the list. - var index = tracking == null ? 0 : (Behaviors.IndexOf(tracking) + 1); - Behaviors.Insert(index, behavior); + Behaviors.Insert(++index, behavior); return behavior; }); @@ -70,9 +79,12 @@ void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) switch (e.Action) { case NotifyCollectionChangedAction.Add: - // Can't have more than one of MockTrackingBehavior, since that causes problems. - if (Behaviors.OfType().Skip(1).Any()) - throw new InvalidOperationException(Resources.DuplicateTrackingBehavior); + // Can't have more than one of MockContextBehavior, since that causes problems. + if (Behaviors.OfType().Skip(1).Any()) + throw new InvalidOperationException(Resources.DuplicateContextBehavior); + // Can't have more than one of MockRecordingBehavior, since that causes problems. + if (Behaviors.OfType().Skip(1).Any()) + throw new InvalidOperationException(Resources.DuplicateRecordingBehavior); foreach (var behavior in e.NewItems.OfType()) setupBehaviorMap.TryAdd(behavior.Setup, behavior); diff --git a/src/Moq/Moq.Sdk/IMock.cs b/src/Moq/Moq.Sdk/IMock.cs index 2f29cdeb..7cd3fc7c 100644 --- a/src/Moq/Moq.Sdk/IMock.cs +++ b/src/Moq/Moq.Sdk/IMock.cs @@ -27,7 +27,7 @@ public interface IMock : IStunt /// /// Arbitrary state associated with a mock instance. /// - MockState State { get; } + StateBag State { get; set; } /// /// The list of mock behavior pipelines configured for this mock. diff --git a/src/Moq/Moq.Sdk/IMockSetup.cs b/src/Moq/Moq.Sdk/IMockSetup.cs index be37ff55..98e5da0b 100644 --- a/src/Moq/Moq.Sdk/IMockSetup.cs +++ b/src/Moq/Moq.Sdk/IMockSetup.cs @@ -20,6 +20,17 @@ public interface IMockSetup : IEquatable, IFluentInterface /// IArgumentMatcher[] Matchers { get; } + /// + /// Gets or sets an occurrence constraint placed on the setup, for matching + /// or verification purposes. + /// + Times? Occurrence { get; set; } + + /// + /// Arbitrary state associated with a setup. + /// + StateBag State { get; } + /// /// Tests whether the setup applies to an actual invocation. /// diff --git a/src/Moq/Moq.Sdk/IMock`1.cs b/src/Moq/Moq.Sdk/IMock`1.cs index 95bea244..42aa83ef 100644 --- a/src/Moq/Moq.Sdk/IMock`1.cs +++ b/src/Moq/Moq.Sdk/IMock`1.cs @@ -3,7 +3,7 @@ /// /// Provides introspection information about a mock. /// - public interface IMock : IMock, IFluentInterface + public interface IMock : IMock, IFluentInterface { /// /// The mock object this introspection data belongs to. diff --git a/src/Moq/Moq.Sdk/MockContext.cs b/src/Moq/Moq.Sdk/MockContext.cs index a11f1ce0..adfb71ea 100644 --- a/src/Moq/Moq.Sdk/MockContext.cs +++ b/src/Moq/Moq.Sdk/MockContext.cs @@ -10,7 +10,7 @@ public static class MockContext { /// /// The most recent invocation performed on the mock, tracked - /// by the . + /// by the . /// public static IMethodInvocation CurrentInvocation { @@ -26,7 +26,7 @@ public static IMethodInvocation CurrentInvocation /// /// /// This property is also tracked and populated by the - /// . + /// . /// public static IMockSetup CurrentSetup { diff --git a/src/Moq/Moq.Sdk/MockTrackingBehavior.cs b/src/Moq/Moq.Sdk/MockContextBehavior.cs similarity index 76% rename from src/Moq/Moq.Sdk/MockTrackingBehavior.cs rename to src/Moq/Moq.Sdk/MockContextBehavior.cs index 00178488..e06d124e 100644 --- a/src/Moq/Moq.Sdk/MockTrackingBehavior.cs +++ b/src/Moq/Moq.Sdk/MockContextBehavior.cs @@ -1,19 +1,11 @@ using System; using Stunts; using System.Diagnostics; -using System.Linq; -using System.Collections.Generic; -using System.Reflection; -using System.IO; -using System.Runtime.Versioning; -using System.Runtime.InteropServices; -using System.Text; namespace Moq.Sdk { /// - /// Core behavior that allows tracking invocations and - /// building setups from them. + /// Core behavior that allows tracking invocations and building setups from them. /// /// Sets the /// as well as the , used in @@ -21,7 +13,7 @@ namespace Moq.Sdk /// respectively. /// /// - public class MockTrackingBehavior : IStuntBehavior + public class MockContextBehavior : IStuntBehavior { /// /// Returns since it tracks all invocations. @@ -43,10 +35,6 @@ public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) // current setup being performed via the MockContext. MockContext.CurrentSetup = MockSetup.Freeze(invocation); - // Only record the invocation if it's *not* performed within a setup scope. - if (!SetupScope.IsActive) - invocation.Target.AsMock().Invocations.Add(invocation); - // While debugging, capture invocation stack traces for easier // troubleshooting if (Debugger.IsAttached) diff --git a/src/Moq/Moq.Sdk/MockDecorator.cs b/src/Moq/Moq.Sdk/MockDecorator.cs index 8fb374f4..dd329998 100644 --- a/src/Moq/Moq.Sdk/MockDecorator.cs +++ b/src/Moq/Moq.Sdk/MockDecorator.cs @@ -30,7 +30,11 @@ public abstract class MockDecorator : IMock /// /// See . /// - public virtual MockState State => mock.State; + public virtual StateBag State + { + get => mock.State; + set => mock.State = value; + } /// /// See . diff --git a/src/Moq/Moq.Sdk/MockExtensions.cs b/src/Moq/Moq.Sdk/MockExtensions.cs index aca814c1..3ae45d36 100644 --- a/src/Moq/Moq.Sdk/MockExtensions.cs +++ b/src/Moq/Moq.Sdk/MockExtensions.cs @@ -21,6 +21,33 @@ public static class MockExtensions public static IMock AsMock(this T instance) => (instance as IMocked)?.Mock.As(instance) ?? throw new ArgumentException(Strings.TargetNotMock, nameof(instance)); + /// + /// Clones a mock by creating a new instance of the + /// from and copying its behaviors, invocations and state. + /// + public static IMock Clone(this IMock mock) + { + if (!mock.State.TryGetValue(".ctor", out var ctor)) + throw new ArgumentException("No constructor state found for cloning."); + + var clone = ((IMocked)Activator.CreateInstance(mock.Object.GetType(), ctor)).Mock; + clone.State = mock.State.Clone(); + + clone.Behaviors.Clear(); + foreach (var behavior in mock.Behaviors) + { + clone.Behaviors.Add(behavior); + } + + clone.Invocations.Clear(); + foreach (var invocation in mock.Invocations) + { + clone.Invocations.Add(invocation); + } + + return ((T)clone.Object).AsMock(); + } + /// /// Gets the invocations performed on the mock so far that match the given /// setup lambda. @@ -68,7 +95,11 @@ public Mock(IMock mock, T target) public ICollection Invocations => mock.Invocations; - public MockState State => mock.State; + public StateBag State + { + get => mock.State; + set => mock.State = value; + } public IEnumerable Setups => mock.Setups; diff --git a/src/Moq/Moq.Sdk/MockFactory.cs b/src/Moq/Moq.Sdk/MockFactory.cs index b103f6e4..40392a33 100644 --- a/src/Moq/Moq.Sdk/MockFactory.cs +++ b/src/Moq/Moq.Sdk/MockFactory.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using Stunts; namespace Moq.Sdk { @@ -20,12 +21,31 @@ public class MockFactory : IMockFactory /// /// See /// - public object CreateMock(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] construtorArguments) + public object CreateMock(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] constructorArguments) { - var name = MockNaming.GetFullName(baseType, implementedInterfaces); - var type = mocksAssembly.GetType(name, true, false); + var type = GetMockType(mocksAssembly, baseType, implementedInterfaces, constructorArguments); + var mocked = CreateMock(type, constructorArguments); + + // Save for cloning purposes. + mocked.Mock.State.Set(".ctor", constructorArguments); + + return mocked; + } - return (IMocked)Activator.CreateInstance(type, construtorArguments); + /// + /// Creates an instance of the mock, which must implement interface. + /// + protected IMocked CreateMock(Type type, object[] constructorArguments) + => (IMocked)Activator.CreateInstance(type, constructorArguments); + + /// + /// Default implementation for locating mock types will use to + /// create a candidate mock type name, then get it from the . + /// + protected virtual Type GetMockType(Assembly mocksAssembly, Type baseType, Type[] implementedInterfaces, object[] constructorArguments) + { + var name = MockNaming.GetFullName(baseType, implementedInterfaces); + return mocksAssembly.GetType(name, true, false); } } } \ No newline at end of file diff --git a/src/Moq/Moq.Sdk/MockRecordingBehavior.cs b/src/Moq/Moq.Sdk/MockRecordingBehavior.cs new file mode 100644 index 00000000..d4cc5852 --- /dev/null +++ b/src/Moq/Moq.Sdk/MockRecordingBehavior.cs @@ -0,0 +1,29 @@ +using System; +using Stunts; +using System.Diagnostics; + +namespace Moq.Sdk +{ + /// + /// Records invocations performed on the mock, as long as + /// is . + /// + public class MockRecordingBehavior : IStuntBehavior + { + /// + /// Returns if is , + /// since it will records all invocations in that case. + /// + public bool AppliesTo(IMethodInvocation invocation) => !SetupScope.IsActive; + + /// + /// Implements the tracking of invocations for the excuted invocations. + /// + public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) + { + invocation.Target.AsMock().Invocations.Add(invocation); + + return next().Invoke(invocation, next); + } + } +} \ No newline at end of file diff --git a/src/Moq/Moq.Sdk/MockSetup.cs b/src/Moq/Moq.Sdk/MockSetup.cs index a82069ff..034baf17 100644 --- a/src/Moq/Moq.Sdk/MockSetup.cs +++ b/src/Moq/Moq.Sdk/MockSetup.cs @@ -36,6 +36,12 @@ public MockSetup(IMethodInvocation invocation, IArgumentMatcher[] matchers) /// public IArgumentMatcher[] Matchers => matchers; + /// + public Times? Occurrence { get; set; } + + /// + public StateBag State { get; } = new StateBag(); + /// public bool AppliesTo(IMethodInvocation actualInvocation) { diff --git a/src/Moq/Moq.Sdk/Properties/Resources.Designer.cs b/src/Moq/Moq.Sdk/Properties/Resources.Designer.cs index 2823ce14..0229dcc5 100644 --- a/src/Moq/Moq.Sdk/Properties/Resources.Designer.cs +++ b/src/Moq/Moq.Sdk/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Moq.Sdk.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -61,11 +61,20 @@ internal Resources() { } /// - /// Looks up a localized string similar to The mock already contains a tracking behavior.. + /// Looks up a localized string similar to The mock already contains a context behavior.. /// - internal static string DuplicateTrackingBehavior { + internal static string DuplicateContextBehavior { get { - return ResourceManager.GetString("DuplicateTrackingBehavior", resourceCulture); + return ResourceManager.GetString("DuplicateContextBehavior", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The mock already contains a recording behavior.. + /// + internal static string DuplicateRecordingBehavior { + get { + return ResourceManager.GetString("DuplicateRecordingBehavior", resourceCulture); } } diff --git a/src/Moq/Moq.Sdk/Properties/Resources.resx b/src/Moq/Moq.Sdk/Properties/Resources.resx index 6698a991..1b5c69cd 100644 --- a/src/Moq/Moq.Sdk/Properties/Resources.resx +++ b/src/Moq/Moq.Sdk/Properties/Resources.resx @@ -126,7 +126,10 @@ The target of the invocation is not a mock. - - The mock already contains a tracking behavior. + + The mock already contains a context behavior. + + + The mock already contains a recording behavior. \ No newline at end of file diff --git a/src/Moq/Moq.Sdk/StackTraceExtensions.cs b/src/Moq/Moq.Sdk/StackTraceExtensions.cs index 655070be..28e6a224 100644 --- a/src/Moq/Moq.Sdk/StackTraceExtensions.cs +++ b/src/Moq/Moq.Sdk/StackTraceExtensions.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.IO; using System.Linq; using System.Text; using Stunts; @@ -16,13 +17,16 @@ public static class StackTraceExtensions public static string GetStackTrace(this IMethodInvocation invocation) { var allFrames = EnhancedStackTrace.Current(); - var invocationFrame = allFrames.First(x => x.GetMethod() == invocation.MethodBase); + var invocationFrame = allFrames.FirstOrDefault(x => x.GetMethod() == invocation.MethodBase); + //if (invocationFrame == null) + // We know that the generated proxies live in the same assembly as the tests, so we use that // to scope the stack trace from the current invocation method up to the top call (test method) var testFrame = allFrames.LastOrDefault(x => x.GetMethod().DeclaringType.Assembly == invocation.MethodBase.DeclaringType.Assembly); var sb = new StringBuilder(); - var appendLine = false; + // Always append if we didn't find the tip invocation frame + var appendLine = invocationFrame == null; foreach (var frame in allFrames) { if (!appendLine && frame == invocationFrame) @@ -34,7 +38,7 @@ public static string GetStackTrace(this IMethodInvocation invocation) var filePath = frame.GetFileName(); if (!string.IsNullOrEmpty(filePath)) { - sb.Append(EnhancedStackTrace.TryGetFullPath(filePath)); + sb.Append(EnhancedStackTrace.TryGetFullPath(filePath).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)); var lineNo = frame.GetFileLineNumber(); var colNo = frame.GetFileColumnNumber(); if (lineNo != 0 && colNo != 0) diff --git a/src/Moq/Moq.Sdk/MockState.cs b/src/Moq/Moq.Sdk/StateBag.cs similarity index 89% rename from src/Moq/Moq.Sdk/MockState.cs rename to src/Moq/Moq.Sdk/StateBag.cs index 44b3a14f..8651a822 100644 --- a/src/Moq/Moq.Sdk/MockState.cs +++ b/src/Moq/Moq.Sdk/StateBag.cs @@ -7,11 +7,26 @@ namespace Moq.Sdk /// /// A typed state bag for holding arbitrary mock state. All members are thread-safe. /// - [DebuggerDisplay("Count = {state.Count}", Name = "State", Type = nameof(MockState))] - public class MockState + [DebuggerDisplay("Count = {state.Count}", Name = "State", Type = nameof(StateBag))] + public class StateBag { [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - ConcurrentDictionary state = new ConcurrentDictionary(); + ConcurrentDictionary state; + + /// + /// Creates a new instance of the state bag. + /// + public StateBag() + : this(new ConcurrentDictionary()) + { + } + + private StateBag(ConcurrentDictionary initialState) => state = new ConcurrentDictionary(initialState); + + /// + /// Creates a copy of the state bag. + /// + public StateBag Clone() => new StateBag(state); /// /// Clears the state. @@ -95,7 +110,10 @@ public void Set(object key, T value) public bool TryGetValue(out T value) { var result = state.TryGetValue(typeof(T), out var _value); - value = (T)_value; + value = default; + if (result && _value != null) + value = (T)_value; + return result; } @@ -111,7 +129,10 @@ public bool TryGetValue(out T value) public bool TryGetValue(object key, out T value) { var result = state.TryGetValue(Key(key), out var _value); - value = (T)_value; + value = default; + if (result && _value != null) + value = (T)_value; + return result; } @@ -126,7 +147,10 @@ public bool TryGetValue(object key, out T value) public bool TryRemove(out T value) { var result = state.TryRemove(typeof(T), out var _value); - value = (T)_value; + value = default; + if (result && _value != null) + value = (T)_value; + return result; } @@ -142,7 +166,10 @@ public bool TryRemove(out T value) public bool TryRemove(object key, out T value) { var result = state.TryRemove(Key(key), out var _value); - value = (T)_value; + value = default; + if (result && _value != null) + value = (T)_value; + return result; } @@ -178,6 +205,6 @@ public bool TryRemove(object key, out T value) /// /// Gets the key to use depending on the received . /// - object Key(object key) => typeof(T) == typeof(object) ? key : Tuple.Create(typeof(T), key); + object Key(object key) => typeof(T) == typeof(object) ? key : (Key: key, Type: typeof(T)); } } \ No newline at end of file diff --git a/src/Moq/Moq.Sdk/Times.cs b/src/Moq/Moq.Sdk/Times.cs new file mode 100644 index 00000000..8b61ce77 --- /dev/null +++ b/src/Moq/Moq.Sdk/Times.cs @@ -0,0 +1,219 @@ +using System; +using System.ComponentModel; +using Stunts; + +namespace Moq.Sdk +{ + /// + /// Defines the number of expected invocations. + /// + public readonly struct Times : IEquatable + { + readonly Lazy hashCode; + + /// + /// Initializes the constraint with the given and + /// values. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Times(int from, int to) + { + From = from; + To = to; + hashCode = new Lazy(() => new HashCode().AddRange(from, to).ToHashCode()); + } + + /// + /// At least one call is expected. + /// + public static Times AtLeastOnce { get; } = new Times(1, int.MaxValue); + + /// + /// At most one call is expected. + /// + public static Times AtMostOnce { get; } = new Times(0, 1); + + /// + /// No calls are expected. + /// + public static Times Never { get; } = new Times(0, 0); + + /// + /// Exactly one call is expected. + /// + public static Times Once { get; } = new Times(1, 1); + + /// + /// The minimum calls expected. + /// + public int From { get; } + + /// + /// The maximum calls expected. + /// + public int To { get; } + + /// Deconstructs this instance. + /// This output parameter will receive the minimum required number of calls satisfying this instance (i.e. the lower inclusive bound). + /// This output parameter will receive the maximum allowed number of calls satisfying this instance (i.e. the upper inclusive bound). + [EditorBrowsable(EditorBrowsableState.Never)] + public void Deconstruct(out int from, out int to) + { + if (hashCode == null) + { + // default(Times) == AtLeastOnce + AtLeastOnce.Deconstruct(out from, out to); + } + else + { + from = From; + to = To; + } + } + + /// + /// At least calls are expected. + /// + /// The minimum number of expected calls. + public static Times AtLeast(int count) + { + if (count < 1) + throw new ArgumentOutOfRangeException(nameof(count)); + + return new Times(count, int.MaxValue); + } + + /// + /// At most calls are expected. + /// + /// The maximum number of expected calls. + public static Times AtMost(int count) + { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + + return new Times(0, count); + } + + /// + /// Exactly call is expected. + /// + /// The times that a method or property can be called. + public static Times Exactly(int count) + { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + + return new Times(count, count); + } + + /// + /// Validates whether the given value satisfies this + /// occurrence constraint. + /// + /// The number of calls to validate against this occurrence constraint. + /// + /// if is included in the range - + /// (inclusive). + /// + public bool Validate(int count) + { + // By deconstructing, we apply our default behavior of considering default(Times) == AtLeastOnce. + var (from, to) = this; + return count >= from && count <= to; + } + + /// + /// Returns a value indicating whether this instance is equal to a specified value. + /// + /// A value to compare to this instance. + /// + /// if has the same value as this instance; + /// otherwise, . + /// + public bool Equals(Times other) + { + // By deconstructing first, we apply the behavior for default(Times) == AtLeastOnce + var (from, to) = this; + var (otherFrom, otherTo) = other; + + return from == otherFrom && to == otherTo; + } + + /// + /// Returns a value indicating whether this instance is equal to a specified value. + /// + /// An object to compare to this instance. + /// + /// if has the same value as this instance; + /// otherwise, . + /// s + public override bool Equals(object obj) => obj is Times other && Equals(other); + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms + /// and data structures like a hash table. + /// + public override int GetHashCode() => hashCode == null ? AtLeastOnce.GetHashCode() : hashCode.Value; + + /// + /// Determines whether two specified objects have the same value. + /// + /// The first . + /// The second . + /// + /// if has the same value as ; + /// otherwise, . + /// + public static bool operator ==(Times left, Times right) => left.Equals(right); + + /// + /// Determines whether two specified objects have different values. + /// + /// The first . + /// The second . + /// + /// if the value of is different from + /// 's; otherwise, . + /// + public static bool operator !=(Times left, Times right) => !left.Equals(right); + + /// + /// Converts an integer to if is -1, + /// if is 0, + /// if is 1 and + /// with otherwise. + /// + public static implicit operator Times(int count) => count switch + { + -1 => AtLeastOnce, + 0 => Never, + 1 => Once, + _ => Exactly(count) + }; + + /// + /// Provide a friendly representation of the expected range. + /// + public override string ToString() + { + if (this == Once) + return nameof(Once); + if (this == Never) + return nameof(Never); + if (this == AtLeastOnce) + return nameof(AtLeastOnce); + if (this == AtMostOnce) + return nameof(AtMostOnce); + if (From == 0 && To != 0) + return $"{nameof(AtMost)}({To})"; + if (From != 0 && To == int.MaxValue) + return $"{nameof(AtLeast)}({From})"; + + return $"{From}..{To}"; + } + } +} diff --git a/src/Moq/Moq.Tests/DynamicMockTests.cs b/src/Moq/Moq.Tests/DynamicMockTests.cs index fca6b024..5cc9bef6 100644 --- a/src/Moq/Moq.Tests/DynamicMockTests.cs +++ b/src/Moq/Moq.Tests/DynamicMockTests.cs @@ -12,7 +12,7 @@ public class DynamicMockTests public async Task WhenAddingMockBehavior_ThenCanInterceptSelectively() { var calculator = await new DynamicMock(LanguageNames.CSharp).CreateAsync(); - var behavior = new MockTrackingBehavior(); + var behavior = new MockContextBehavior(); calculator.AddBehavior(behavior); calculator.AddBehavior((m, n) => m.CreateValueReturn(CalculatorMode.Scientific), m => m.MethodBase.Name == "get_Mode"); diff --git a/src/Moq/Moq.Tests/MoqTests.cs b/src/Moq/Moq.Tests/MoqTests.cs index cf4c1abb..4e94c9e5 100644 --- a/src/Moq/Moq.Tests/MoqTests.cs +++ b/src/Moq/Moq.Tests/MoqTests.cs @@ -5,11 +5,17 @@ using static Moq.Syntax; using Stunts; using Sample; +using System.Linq; +using Xunit.Abstractions; namespace Moq.Tests { public class MoqTests { + ITestOutputHelper output; + + public MoqTests(ITestOutputHelper output) => this.output = output; + [Fact] public void CanRaiseEvents() { diff --git a/src/Moq/Moq.Tests/VerificationTests.cs b/src/Moq/Moq.Tests/VerificationTests.cs new file mode 100644 index 00000000..44bb632d --- /dev/null +++ b/src/Moq/Moq.Tests/VerificationTests.cs @@ -0,0 +1,325 @@ +using System; +using Moq.Sdk; +using Sample; +using Xunit; +using Xunit.Abstractions; +using static Moq.Syntax; + +namespace Moq.Tests +{ + public class VerificationTests + { + readonly ITestOutputHelper output; + + public VerificationTests(ITestOutputHelper output) => this.output = output; + + [Fact] + public void VerifySyntaxOnceOnSetup() + { + var calculator = Mock.Of(); + + calculator.Add(2, 3).Returns(5).Once(); + + Assert.ThrowsAny(() => Verify(calculator)); + + calculator.Add(2, 3); + + // TODO: We should have a this.Verify() extension method for backs compat. + Verify(calculator); + + calculator.Add(2, 3); + + Assert.ThrowsAny(() => Verify(calculator)); + } + + [Fact] + public void VerifySyntaxOnceOnVerify() + { + var calculator = Mock.Of(); + + Assert.Throws(() => Verify(calculator).Add(2, 3).Once()); + + calculator.Add(2, 3); + + Verify(calculator).Add(2, 3).Once(); + + calculator.Add(2, 3); + + Assert.Throws(() => Verify(calculator).Add(2, 3).Once()); + } + + [Fact] + public void VerifySyntaxNeverOnSetup() + { + var calculator = Mock.Of(); + + calculator.Add(2, 3).Returns(5).Never(); + + Verify(calculator); + + calculator.Add(2, 3); + + Assert.Throws(() => Verify(calculator)); + } + + [Fact] + public void VerifySyntaxNeverOnVerify() + { + var calculator = Mock.Of(); + + calculator.Add(2, 3).Returns(5); + + Verify(calculator).Add(2, 3).Never(); + + calculator.Add(2, 3); + + Assert.Throws(() => Verify(calculator).Add(2, 3).Never()); + } + + [Fact] + public void VerifySyntaxExactlyOnSetup() + { + var calculator = Mock.Of(); + + calculator.Add(2, 3).Returns(5).Exactly(2); + calculator.Add(2, 3); + + Assert.Throws(() => Verify(calculator)); + + calculator.Add(2, 3); + + Verify(calculator); + + calculator.Add(2, 3); + + Assert.Throws(() => Verify(calculator)); + } + + [Fact] + public void VerifySyntaxExactlyOnVerify() + { + var calculator = Mock.Of(); + + calculator.Add(2, 3).Returns(5); + calculator.Add(2, 3); + + Assert.Throws(() => Verify(calculator).Add(2, 3).Exactly(2)); + + calculator.Add(2, 3); + + Verify(calculator).Add(2, 3).Exactly(2); + + calculator.Add(2, 3); + + Assert.Throws(() => Verify(calculator).Add(2, 3).Never()); + } + + [Fact] + public void VerifyPropertySet() + { + var calculator = Mock.Of(); + + Assert.Throws(() => Verify.Called(() => calculator.Mode = CalculatorMode.Scientific)); + + calculator.Mode = CalculatorMode.Scientific; + + Verify.Called(() => calculator.Mode = CalculatorMode.Scientific); + } + + [Fact] + public void VerifyVoidMethod() + { + var calculator = Mock.Of(); + + Assert.Throws(() => Verify.Called(() => calculator.TurnOn())); + + calculator.TurnOn(); + + Verify.Called(() => calculator.TurnOn()); + + Assert.Throws(() => Verify.Called(() => calculator.TurnOn(), 2)); + + calculator.TurnOn(); + + Verify.Called(() => calculator.TurnOn(), 2); + } + + [Fact] + public void VerifyNotCalled() + { + var calculator = Mock.Of(); + + Verify.NotCalled(() => calculator.TurnOn()); + Verify.NotCalled(() => calculator.Add(2, 3)); + + calculator.TurnOn(); + calculator.Add(2, 3); + + Assert.Throws(() => Verify.NotCalled(() => calculator.TurnOn())); + Assert.Throws(() => Verify.NotCalled(() => calculator.Add(2, 3))); + } + + [Fact] + public void VerifyNotCalledFluent() + { + var calculator = Mock.Of(); + + Verify.NotCalled(calculator).TurnOn(); + Verify.NotCalled(calculator).Add(2, 3); + + calculator.TurnOn(); + calculator.Add(2, 3); + + Assert.Throws(() => Verify.NotCalled(calculator).TurnOn()); + Assert.Throws(() => Verify.NotCalled(calculator).Add(2, 3)); + } + + [Fact] + public void VerifyCalls() + { + var calculator = Mock.Of(); + + calculator.Setup(c => c.TurnOn()).Once(); + calculator.Add(2, 3).Returns(5).Once(); + + Assert.Throws(() => Verify.Calls(calculator)); + + calculator.TurnOn(); + calculator.Add(2, 3); + + Verify.Calls(calculator); + } + + [Fact] + public void VerifyCallsCustom() + { + var calculator = Mock.Of(); + + calculator.Setup(c => c.TurnOn()).Once(); + calculator.Add(2, 3).Returns(5).Once(); + + Verify.Calls( + () => calculator.TurnOn(), + calls => Assert.Empty(calls)); + + calculator.TurnOn(); + + Verify.Calls( + () => calculator.TurnOn(), + calls => Assert.Single(calls)); + } + + [Fact] + public void VerifyExtensionAction() + { + var calculator = Mock.Of(); + + // At least once + Assert.Throws(() => calculator.Verify(x => x.TurnOn())); + // Once + Assert.Throws(() => calculator.Verify(x => x.TurnOn(), 1)); + // At least once with message + Assert.Throws(() => calculator.Verify(x => x.TurnOn(), "Should have been called!")); + // Once with message + Assert.Throws(() => calculator.Verify(x => x.TurnOn(), 1, "Should have been called!")); + // Times.Once with message + Assert.Throws(() => calculator.Verify(x => x.TurnOn(), Times.Once, "Should have been called!")); + + calculator.TurnOn(); + + // At least once + calculator.Verify(x => x.TurnOn()); + // Once + calculator.Verify(x => x.TurnOn(), 1); + // At least once with message + calculator.Verify(x => x.TurnOn(), "Should have been called!"); + // Once with message + calculator.Verify(x => x.TurnOn(), 1, "Should have been called!"); + // Times.Once with message + calculator.Verify(x => x.TurnOn(), Times.Once, "Should have been called!"); + } + + [Fact] + public void VerifyExtensionFunction() + { + var calculator = Mock.Of(); + + // At least once + Assert.Throws(() => calculator.Verify(x => x.Add(2, 3))); + // Once + Assert.Throws(() => calculator.Verify(x => x.Add(2, 3), 1)); + // At least once with message + Assert.Throws(() => calculator.Verify(x => x.Add(2, 3), "Should have been called!")); + // Once with message + Assert.Throws(() => calculator.Verify(x => x.Add(2, 3), 1, "Should have been called!")); + // Times.Once with message + Assert.Throws(() => calculator.Verify(x => x.Add(2, 3), Times.Once, "Should have been called!")); + + calculator.Add(2, 3); + + // At least once + calculator.Verify(x => x.Add(2, 3)); + // Once + calculator.Verify(x => x.Add(2, 3), 1); + // At least once with message + calculator.Verify(x => x.Add(2, 3), "Should have been called!"); + // Once with message + calculator.Verify(x => x.Add(2, 3), 1, "Should have been called!"); + // Times.Once with message + calculator.Verify(x => x.Add(2, 3), Times.Once, "Should have been called!"); + } + + //[Fact] + public void CanVerify() + { + var calculator = Mock.Of(); + + calculator.Add(2, 3).Returns(5).Once(); + calculator.Mode.Returns(CalculatorMode.Scientific).Exactly(2); + + var mock = Mock.Get(calculator); + foreach (var invocation in mock.Invocations) + { + output.WriteLine((string)invocation.Context[nameof(Environment.StackTrace)]); + } + + // Syntax-based, follows straightforward setup approach, no lambdas. + Verify(calculator).Mode = CalculatorMode.Scientific; + Verify(calculator).Add(2, 3); + + // Verify all "verifiable" calls, i.e. those with + // a Once/Never/etc. setup. + Verify(calculator); + // Long form, with no lambda + Verify.Called(calculator); + + // equivalent non-syntax version + //calculator.Verify().Add(2, 3); + Verify(calculator).Add(1, 1).Never(); + + // Explicit Verify, still no lambdas, long form of Syntax + Verify.Called(calculator).Add(2, 3).Exactly(2); + // Explicit Verify, still no lambdas, long form of Syntax + Verify.NotCalled(calculator).Add(2, 3); + + // For the case where you want keep the mock in "running" mode after the verify. + + Verify.Called(() => calculator.Add(2, 3).Once()); + Verify.NotCalled(() => calculator.TurnOn()); + + // More advanced verification, access calls via lambda + Verify.Calls( + () => calculator.Add(2, 3), + calls => Assert.Single(calls)); + // Works for void/action + Verify.Calls( + () => calculator.TurnOn(), + calls => Assert.Single(calls)); + + // Verify all "verifiable" calls, i.e. those with + // a Once/Never/etc. setup. + Verify.Calls(calculator); + // calculator.Verify(); + } + } +} diff --git a/src/Moq/Moq/MockInitializer.cs b/src/Moq/Moq/MockInitializer.cs index 4aa00052..182ce8f6 100644 --- a/src/Moq/Moq/MockInitializer.cs +++ b/src/Moq/Moq/MockInitializer.cs @@ -1,8 +1,5 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; +using System.ComponentModel; using System.Linq; -using Moq.Properties; using Moq.Sdk; using Stunts; @@ -10,7 +7,7 @@ namespace Moq { /// /// Provides the method for configuring the initial - /// behaviors set of behaviors for a given . + /// set of behaviors for a given . /// [EditorBrowsable(EditorBrowsableState.Never)] public static class MockInitializer @@ -29,7 +26,8 @@ public static void Initialize(this IMocked mocked, MockBehavior behavior) { mocked.Mock.Behaviors.Clear(); - mocked.Mock.Behaviors.Add(new MockTrackingBehavior()); + mocked.Mock.Behaviors.Add(new MockContextBehavior()); + mocked.Mock.Behaviors.Add(new MockRecordingBehavior()); mocked.Mock.Behaviors.Add(new EventBehavior()); mocked.Mock.Behaviors.Add(new PropertyBehavior { SetterRequiresSetup = behavior == MockBehavior.Strict }); mocked.Mock.Behaviors.Add(new DefaultEqualityBehavior()); diff --git a/src/Moq/Moq/Moq.csproj b/src/Moq/Moq/Moq.csproj index b804b18c..7905add8 100644 --- a/src/Moq/Moq/Moq.csproj +++ b/src/Moq/Moq/Moq.csproj @@ -20,6 +20,14 @@ + + + + + + + + True diff --git a/src/Moq/Moq/OccurrenceExtension.cs b/src/Moq/Moq/OccurrenceExtension.cs new file mode 100644 index 00000000..e0c7ec31 --- /dev/null +++ b/src/Moq/Moq/OccurrenceExtension.cs @@ -0,0 +1,122 @@ +using System.ComponentModel; +using System.Linq; +using Moq.Sdk; + +namespace Moq +{ + /// + /// Extensions for specifying occurrence for behavior specification + /// or verification. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class OccurrenceExtension + { + /// + /// Supports legacy API and forwards to . + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static TResult Verifiable(this TResult target) => AtLeastOnce(target); + + /// + /// Specifies that the current fluent invocation is expected to be + /// called at least once. + /// + public static TResult AtLeastOnce(this TResult target) + { + var setup = MockContext.CurrentSetup; + if (setup != null) + { + setup.Occurrence = Times.AtLeastOnce; + var mock = setup.Invocation.Target.AsMock(); + if (Verify.IsVerifying(mock)) + { + var calls = mock.Invocations.Where(call => setup.AppliesTo(call)); + if (!calls.Any()) + throw new VerifyException(mock, setup); + } + } + else + { + // TODO: throw if no setup? + } + + return default; + } + + /// + /// Specifies that the current fluent invocation is expected to be + /// called exactly once. + /// + public static TResult Once(this TResult target) + { + var setup = MockContext.CurrentSetup; + if (setup != null) + { + setup.Occurrence = Times.Once; + var mock = setup.Invocation.Target.AsMock(); + if (Verify.IsVerifying(mock)) + { + var calls = mock.Invocations.Where(call => setup.AppliesTo(call)).Take(2).ToArray(); + if (calls.Length != 1) + throw new VerifyException(mock, setup); + } + } + else + { + // TODO: throw if no setup? + } + + return default; + } + + /// + /// Specifies that the current fluent invocation is expected to never + /// be called. + /// + public static TResult Never(this TResult target) + { + var setup = MockContext.CurrentSetup; + if (setup != null) + { + setup.Occurrence = Times.Never; + var mock = setup.Invocation.Target.AsMock(); + if (Verify.IsVerifying(mock)) + { + if (mock.Invocations.Where(call => setup.AppliesTo(call)).Any()) + throw new VerifyException(mock, setup); + } + } + else + { + // TODO: throw if no setup? + } + + return target; + } + + /// + /// Specifies that the current fluent invocation is expected to be + /// called exactly the given number of times. + /// + public static TResult Exactly(this TResult target, int callCount) + { + var setup = MockContext.CurrentSetup; + if (setup != null) + { + setup.Occurrence = Times.Exactly(callCount); + var mock = setup.Invocation.Target.AsMock(); + if (Verify.IsVerifying(mock)) + { + if (mock.Invocations.Where(call => setup.AppliesTo(call)).Count() != callCount) + throw new VerifyException(mock, setup); + } + } + else + { + // TODO: throw if no setup? + } + + return target; + } + } +} \ No newline at end of file diff --git a/src/Moq/Moq/RecursiveMockBehavior.cs b/src/Moq/Moq/RecursiveMockBehavior.cs index 33ab0259..5b0773d0 100644 --- a/src/Moq/Moq/RecursiveMockBehavior.cs +++ b/src/Moq/Moq/RecursiveMockBehavior.cs @@ -46,11 +46,12 @@ public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) new Type[0], new object[0])).Mock; - // Clone the current mock's behaviors, except for the setups and the tracking - // behavior which is added already by default. + // Clone the current mock's behaviors, except for the setups and the + // context and recording behaviors which are added already by default. foreach (var behavior in currentMock.Behaviors.Where(x => !(x is IMockBehaviorPipeline) && - !(x is MockTrackingBehavior))) + !(x is MockContextBehavior) && + !(x is MockRecordingBehavior))) { recursiveMock.Behaviors.Add(behavior); } diff --git a/src/Moq/Moq/Syntax.cs b/src/Moq/Moq/Syntax.cs index ccc0d21a..6c0d1f87 100644 --- a/src/Moq/Moq/Syntax.cs +++ b/src/Moq/Moq/Syntax.cs @@ -1,5 +1,4 @@ using System; -using Moq.Sdk; namespace Moq { @@ -11,6 +10,12 @@ namespace Moq /// public static class Syntax { + /// + /// Verifies all occurrence constraints on the given mock' setups, and + /// allows further verification on the returned instance. + /// + public static T Verify(T mock) => Moq.Verify.Called(mock); + /// /// Matches any value of the given type. /// diff --git a/src/Moq/Moq/Verify.cs b/src/Moq/Moq/Verify.cs new file mode 100644 index 00000000..8f791b0e --- /dev/null +++ b/src/Moq/Moq/Verify.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq.Sdk; +using Stunts; + +namespace Moq +{ + /// + /// Verifies calls to mocks. + /// + public static class Verify + { + /// + /// Gets whether the given mock is being verified. + /// + internal static bool IsVerifying(IMock mock) + => mock.State.TryGetValue(typeof(Verify), out var verifying) && verifying; + + /// + /// Verifies all setups that had an occurrence constraint applied, + /// and allows specific verifications to be performed on the returned + /// object too. + /// + /// An object that can be used to perform additional call verifications. + public static T Called(T target) => Calls(target); + + /// + /// Verifies a method invocation matching the was executed + /// the specified number of . + /// + /// The method invocation to match against actual calls. + /// Number of times the method should have been called. + public static void Called(Func function, int times) => Called(function, (Times)times); + + /// + /// Verifies a method invocation matching the was called at + /// least once. + /// + /// The method invocation to match against actual calls. + /// User message to show. + public static void Called(Func function, string message) => Called(function, default, message: message); + + /// + /// Verifies a method invocation matching the was executed at + /// least once. If is provided, the number of calls is verified too. + /// + /// The method invocation to match against actual calls. + /// Optional number of times the method should have been called. Defaults to . + /// An integer value can also be specificed since there is built-in conversion support from integer to . + /// Optional user message to show. + public static void Called(Func function, Times times = default, string message = null) + { + using (new SetupScope()) + { + function(); + var setup = MockContext.CurrentSetup; + var mock = MockContext.CurrentInvocation.Target.AsMock(); + var calls = mock.Invocations.Where(x => setup.AppliesTo(x)); + if (!times.Validate(calls.Count())) + throw new VerifyException(mock, setup, message); + } + } + + /// + /// Verifies a method invocation matching the was executed the + /// given number of times. + /// + /// The method invocation to match against actual calls. + /// Number of times the method should have been called. + public static void Called(Action action, int times) => Called(action, (Times)times); + + /// + /// Verifies a method invocation matching the was executed at + /// least once. + /// + /// The method invocation to match against actual calls. + /// Optional user message to show. + public static void Called(Action action, string message) => Called(action, default, message: message); + + /// + /// Verifies a method invocation matching the was executed at + /// least once. If is provided, the number of calls is verified too. + /// + /// The method invocation to match against actual calls. + /// Optional number of times the method should have been called. Defaults to . + /// An integer value can also be specificed since there is built-in conversion support from integer to . + /// Optional user message to show. + public static void Called(Action action, Times times = default, string message = null) + { + using (new SetupScope()) + { + action(); + var setup = MockContext.CurrentSetup; + var mock = MockContext.CurrentInvocation.Target.AsMock(); + var calls = mock.Invocations.Where(x => setup.AppliesTo(x)); + if (!times.Validate(calls.Count())) + throw new VerifyException(mock, setup, message); + } + } + + /// + /// Verifies all setups that had an occurrence constraint applied, + /// and allows specific verifications to be performed on the returned + /// object too. + /// + /// An object that can be used to perform additional call verifications. + public static T NotCalled(T target) => GetVerifier(GetVerified(target), true); + + /// + /// Verifies a method invocation matching the was never called. + /// + /// The method invocation to match against actual calls. + /// Optional user message to show. + public static void NotCalled(Func function, string message = null) => Called(function, Times.Never, message); + + /// + /// Verifies a method invocation matching the was never called. + /// + /// The method invocation to match against actual calls. + /// Optional user message to show. + public static void NotCalled(Action action, string message = null) => Called(action, Times.Never, message); + + /// + /// Verifies all setups that had an occurrence constraint applied, + /// and allows specific verifications to be performed on the returned + /// object too. + /// + /// An object that can be used to perform additional call verifications. + public static T Calls(T target) => GetVerifier(GetVerified(target)); + + /// + /// Allows performing custom verification against all actual calls that match the + /// method invocation in . + /// + /// The method invocation to match against actual calls. + /// Actual calls that match the given . + public static void Calls(Func function, Action> calls) + { + using (new SetupScope()) + { + function(); + var setup = MockContext.CurrentSetup; + var mock = MockContext.CurrentInvocation.Target.AsMock(); + calls.Invoke(mock.Invocations.Where(x => setup.AppliesTo(x))); + } + } + + /// + /// Allows performing custom verification against all actual calls that match the + /// method invocation in . + /// + /// The method invocation to match against actual calls. + /// Actual calls that match the given . + public static void Calls(Action action, Action> calls) + { + using (new SetupScope()) + { + action(); + var setup = MockContext.CurrentSetup; + var mock = MockContext.CurrentInvocation.Target.AsMock(); + calls.Invoke(mock.Invocations.Where(x => setup.AppliesTo(x))); + } + } + + /// + /// Gets the mock after verifying that all setups that specified occurrence + /// constraints have succeeded. + /// + static IMock GetVerified(T target) + { + var mock = target.AsMock(); + var failures = (from pipeline in mock.Setups + where pipeline.Setup.Occurrence != null + let times = pipeline.Setup.Occurrence.Value + let calls = mock.Invocations.Where(x => pipeline.AppliesTo(x)).ToArray() + where !times.Validate(calls.Length) + select pipeline.Setup + ).ToArray(); + + if (failures.Length > 0) + throw new VerifyException(mock, failures); + + return mock; + } + + /// + /// Gets a clone of the original mock for verification purposes. + /// + /// The mock to be cloned. + /// Whether to add a behavior that verifies the invocations performed on + /// the clone were never performed on the original mock. + /// + static T GetVerifier(IMock mock, bool notCalled = false) + { + // If the mock is already being verified, we don't need to clone again. + if (mock.State.TryGetValue(typeof(Verify), out var verifying) && verifying) + return mock.Object; + + // Otherwise, we create a verification copy that does not record invocations + // and has default behavior. + var clone = mock.Clone(); + + var recording = clone.Behaviors.OfType().FirstOrDefault(); + if (recording != null) + clone.Behaviors.Remove(recording); + + // The target replacer is needed so that whenever we try to get the target object + // and the IMock from it for occurrence verification, we actually get to the actual + // target being verified, not the cloned mock. Otherwise, invocations won't match + // with the setups, since the target would be different. + clone.Behaviors.Insert( + clone.Behaviors.IndexOf(clone.Behaviors.OfType().First()) + 1, + new TargetReplacerBehavior(mock.Object)); + + if (notCalled) + { + clone.Behaviors.Insert( + clone.Behaviors.IndexOf(clone.Behaviors.OfType().First()) + 1, + new NotCalledBehavior()); + } + + // Sets up the right behaviors for a loose mock + new Moq(clone).Behavior = MockBehavior.Loose; + + clone.State.Set(typeof(Verify), true); + + return clone.Object; + } + + class NotCalledBehavior : IStuntBehavior + { + public bool AppliesTo(IMethodInvocation invocation) => true; + + public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) + { + var mock = invocation.Target.AsMock(); + var setup = MockContext.CurrentSetup; + if (mock.Invocations.Where(x => setup.AppliesTo(x)).Any()) + throw new VerifyException(mock, setup); + + return next().Invoke(invocation, next); + } + } + + class TargetReplacerBehavior : IStuntBehavior + { + readonly object target; + + public TargetReplacerBehavior(object target) => this.target = target; + + public bool AppliesTo(IMethodInvocation invocation) => true; + + public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) => next().Invoke(invocation, next); + //public IMethodReturn Execute(IMethodInvocation invocation, GetNextBehavior next) + // => next().Invoke(new MethodInvocation(target, invocation.MethodBase, invocation.Arguments.ToArray()), next); + } + } +} diff --git a/src/Moq/Moq/VerifyException.cs b/src/Moq/Moq/VerifyException.cs index 64312942..ab1d2295 100644 --- a/src/Moq/Moq/VerifyException.cs +++ b/src/Moq/Moq/VerifyException.cs @@ -1,57 +1,42 @@ -using System; -using Moq.Sdk; +using Moq.Sdk; namespace Moq { /// /// A verification failed to match a mock's invocations - /// against a given setup. + /// against the given setup(s). /// public class VerifyException : MockException { /// /// Initializes the exception with the target - /// mock and setup that failed to match invocations. + /// mock and setup(s) that failed to match invocations. /// - public VerifyException(IMock mock, IMockSetup setup) - : base(setup.ToString()) + public VerifyException(IMock mock, IMockSetup setup, string message = null) + : this(mock, new[] { setup }, message) { - Mock = mock; - Setup = setup; } - /// - /// The expected setup that should have matched the - /// invocations on the mock. - /// - public IMockSetup Setup { get; } - - /// - /// The mock that was tested against the given setup. - /// - public IMock Mock { get; } - } - - /// - /// A verification failed to match a mock's invocations - /// against a given expected setup. - /// - /// The type of the mocked instance. - public class VerifyException : VerifyException - { /// /// Initializes the exception with the target - /// mock and setup that failed to match invocations. + /// mock and setup(s) that failed to match invocations. /// - public VerifyException(IMock mock, IMockSetup setup) - : base(mock, setup) + public VerifyException(IMock mock, IMockSetup[] setups, string message = null) + : base(message) { Mock = mock; + Setups = setups; } /// - /// The mock that was tested against the given setup. + /// The expected setups that should have matched the invocations on the mock + /// but didn't. /// - public new IMock Mock { get; } + public IMockSetup[] Setups { get; } + + /// + /// The mock that was tested. + /// + public IMock Mock { get; } } } diff --git a/src/Moq/Moq/VerifyExtension.cs b/src/Moq/Moq/VerifyExtension.cs index a1aeb1b3..c4463a51 100644 --- a/src/Moq/Moq/VerifyExtension.cs +++ b/src/Moq/Moq/VerifyExtension.cs @@ -1,46 +1,77 @@ using System; using System.ComponentModel; -using System.Linq; using Moq.Sdk; namespace Moq { /// - /// Provides mock verification methods. + /// Provides mock instance verification extension methods. /// [EditorBrowsable(EditorBrowsableState.Never)] public static class VerifyExtension { /// - /// Verifies the mock received at least one invocation that matches - /// the given lambda. + /// Verifies a method invocation matching the was executed the + /// given number of times. /// - public static void Verify(this T mock, Action action) - { - using (new SetupScope()) - { - action(mock); - var setup = MockContext.CurrentSetup; - var info = mock.AsMock(); - if (!info.Invocations.Any(i => setup.AppliesTo(i))) - throw new VerifyException(info, setup); - } - } + /// The mock instance to verify. + /// The method invocation to match against actual calls. + /// Number of times the method should have been called. + public static void Verify(this T target, Action action, int times) + => Moq.Verify.Called(() => action(target), times); /// - /// Verifies the mock received at least one invocation that matches - /// the given lambda. + /// Verifies a method invocation matching the was executed at + /// least once. /// - public static void Verify(this T mock, Func function) - { - using (new SetupScope()) - { - function(mock); - var setup = MockContext.CurrentSetup; - var info = mock.AsMock(); - if (!info.Invocations.Any(i => setup.AppliesTo(i))) - throw new VerifyException(info, setup); - } - } + /// The mock instance to verify. + /// The method invocation to match against actual calls. + /// Optional user message to show. + public static void Verify(this T target, Action action, string message) + => Moq.Verify.Called(() => action(target), message); + + /// + /// Verifies a method invocation matching the was executed at + /// least once. If is provided, the number of calls is verified too. + /// + /// The mock instance to verify. + /// The method invocation to match against actual calls. + /// Optional number of times the method should have been called. Defaults to . + /// An integer value can also be specificed since there is built-in conversion support from integer to . + /// Optional user message to show. + public static void Verify(this T target, Action action, Times times = default, string message = null) + => Moq.Verify.Called(() => action(target), times, message); + + /// + /// Verifies a method invocation matching the was executed the + /// given number of times. + /// + /// The mock instance to verify. + /// The method invocation to match against actual calls. + /// Number of times the method should have been called. + public static void Verify(this T target, Func function, int times) + => Moq.Verify.Called(() => function(target), times); + + /// + /// Verifies a method invocation matching the was executed at + /// least once. + /// + /// The mock instance to verify. + /// The method invocation to match against actual calls. + /// Optional user message to show. + public static void Verify(this T target, Func function, string message) + => Moq.Verify.Called(() => function(target), message); + + /// + /// Verifies a method invocation matching the was executed at + /// least once. If is provided, the number of calls is verified too. + /// + /// The mock instance to verify. + /// The method invocation to match against actual calls. + /// Optional number of times the method should have been called. Defaults to . + /// An integer value can also be specificed since there is built-in conversion support from integer to . + /// Optional user message to show. + public static void Verify(this T target, Func function, int times = -1, string message = null) + => Moq.Verify.Called(() => function(target), times, message); } } diff --git a/src/Stunts/Stunts.Tests/InternalTests.cs b/src/Stunts/Stunts.Tests/InternalTests.cs index 9d08b7c1..a9ea4e24 100644 --- a/src/Stunts/Stunts.Tests/InternalTests.cs +++ b/src/Stunts/Stunts.Tests/InternalTests.cs @@ -121,9 +121,13 @@ public static IEnumerable GetPackageVersions() var resource = repo.GetResourceAsync().Result; var metadata = resource.GetMetadataAsync("Microsoft.CodeAnalysis.Workspaces.Common", true, false, new Logger(null), CancellationToken.None).Result; + // 3.1.0 is already stable and we verified we work with it + // Older versions are guaranteed to not change either, so we + // can rely on it working too, since this test passed at some + // point too. return metadata .Select(m => m.Identity) - .Where(m => m.Version >= new NuGetVersion("2.9.0")) + .Where(m => m.Version >= new NuGetVersion("3.1.0")) .Select(v => new object[] { v }); } diff --git a/src/Stunts/Stunts/IMethodInvocation.cs b/src/Stunts/Stunts/IMethodInvocation.cs index e830c304..e0fa64fb 100644 --- a/src/Stunts/Stunts/IMethodInvocation.cs +++ b/src/Stunts/Stunts/IMethodInvocation.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Reflection; diff --git a/src/Stunts/Stunts/MethodInvocation.cs b/src/Stunts/Stunts/MethodInvocation.cs index a136e8e9..c4f275df 100644 --- a/src/Stunts/Stunts/MethodInvocation.cs +++ b/src/Stunts/Stunts/MethodInvocation.cs @@ -3,7 +3,6 @@ using System.Reflection; using System.Text; using System.Linq; -using System.Collections; using System.Runtime.CompilerServices; using TypeNameFormatter; using System.Diagnostics; diff --git a/src/build/Version.targets b/src/build/Version.targets index d73f2311..5f574926 100644 --- a/src/build/Version.targets +++ b/src/build/Version.targets @@ -29,9 +29,13 @@ AssemblyVersion=$(AssemblyVersion)" /> Returns="$(Version)" Condition="'$(GitInfoImported)' == 'true' And '$(ExcludeRestorePackageImports)' != 'true'"> + + $(GitBranch.Replace('/', '_')) + + - +