diff --git a/CHANGELOG.md b/CHANGELOG.md index da19d5691..7ba1ee1ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## Unreleased + +#### Added + +* New method overloads for `It.Is`, `It.IsIn`, and `It.IsNotIn` that compare values using a custom `IEqualityComparer` (@weitzhandler, #1064) + + ## 4.14.5 (2020-07-01) #### Fixed diff --git a/src/Moq/It.cs b/src/Moq/It.cs index 1df3bec47..3b54e3ada 100644 --- a/src/Moq/It.cs +++ b/src/Moq/It.cs @@ -175,6 +175,25 @@ public static TValue Is(Expression> match) Expression.Lambda>(Expression.Call(thisMethod.MakeGenericMethod(typeof(TValue)), match))); } + /// + /// Matches any value that equals the using the . + /// To use the default comparer for the specified object, specify the value inline, + /// i.e. mock.Verify(service => service.DoWork(value)). + /// + /// Use this overload when you specify a value and a comparer. + /// + /// + /// The value to match with. + /// An with which the values should be compared. + /// Type of the argument to check. + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static TValue Is(TValue value, IEqualityComparer comparer) + { + var thisMethod = (MethodInfo)MethodBase.GetCurrentMethod(); + + return Match.Create(actual => comparer.Equals(actual, value), () => It.Is(value, comparer)); + } + /// /// Matches any value that is in the range specified. /// @@ -234,6 +253,17 @@ public static TValue IsIn(IEnumerable items) return Match.Create(value => items.Contains(value), () => It.IsIn(items)); } + /// + /// Matches any value that is present in the sequence specified. + /// + /// The sequence of possible values. + /// An with which the values should be compared. + /// Type of the argument to check. + public static TValue IsIn(IEnumerable items, IEqualityComparer comparer) + { + return Match.Create(value => items.Contains(value, comparer), () => It.IsIn(items, comparer)); + } + /// /// Matches any value that is present in the sequence specified. /// @@ -276,6 +306,17 @@ public static TValue IsNotIn(IEnumerable items) return Match.Create(value => !items.Contains(value), () => It.IsNotIn(items)); } + /// + /// Matches any value that is not found in the sequence specified. + /// + /// The sequence of disallowed values. + /// An with which the values should be compared. + /// Type of the argument to check. + public static TValue IsNotIn(IEnumerable items, IEqualityComparer comparer) + { + return Match.Create(value => !items.Contains(value, comparer), () => It.IsNotIn(items, comparer)); + } + /// /// Matches any value that is not found in the sequence specified. /// diff --git a/tests/Moq.Tests/CustomTypeMatchersFixture.cs b/tests/Moq.Tests/CustomTypeMatchersFixture.cs index 177d32eda..d75e2e39e 100644 --- a/tests/Moq.Tests/CustomTypeMatchersFixture.cs +++ b/tests/Moq.Tests/CustomTypeMatchersFixture.cs @@ -2,6 +2,8 @@ // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Xunit; @@ -160,6 +162,46 @@ public void It_Is_works_with_custom_matcher() Assert.Equal(2, invocationCount); } + [Fact] + public void It_Is_works_with_custom_comparer() + { + var acceptableArg = "FOO"; + + var invocationCount = 0; + var mock = new Mock(); + mock.Setup(m => m.Method(It.Is(acceptableArg, StringComparer.OrdinalIgnoreCase))) + .Callback((object arg) => invocationCount++); + + mock.Object.Method("foo"); + mock.Object.Method("bar"); + Assert.Equal(1, invocationCount); + + mock.Object.Method("FOO"); + mock.Object.Method("foo"); + mock.Object.Method((string)null); + Assert.Equal(3, invocationCount); + } + + [Fact] + public void It_Is_object_works_with_custom_comparer() + { + var acceptableArg = "FOO"; + + var invocationCount = 0; + var mock = new Mock(); + mock.Setup(m => m.Method(It.Is(acceptableArg, new ObjectStringOrdinalIgnoreCaseComparer()))) + .Callback((object arg) => invocationCount++); + + mock.Object.Method("foo"); + mock.Object.Method("bar"); + Assert.Equal(1, invocationCount); + + mock.Object.Method("FOO"); + mock.Object.Method("foo"); + mock.Object.Method((string)null); + Assert.Equal(3, invocationCount); + } + public interface IX { void Method(); @@ -229,5 +271,24 @@ public struct AnyStruct : ITypeMatcher { public bool Matches(Type typeArgument) => typeArgument.IsValueType; } + + public class ObjectStringOrdinalIgnoreCaseComparer : IEqualityComparer + { + private static IEqualityComparer InternalComparer => StringComparer.OrdinalIgnoreCase; + + public new bool Equals(object x, object y) + { + Debug.Assert(x is string && y is string); + + return InternalComparer.Equals((string)x, (string)y); + } + + public int GetHashCode(object obj) + { + Debug.Assert(obj is string); + + return InternalComparer.GetHashCode((string)obj); + } + } } } diff --git a/tests/Moq.Tests/MatchersFixture.cs b/tests/Moq.Tests/MatchersFixture.cs index 2c50985c9..9d9579575 100644 --- a/tests/Moq.Tests/MatchersFixture.cs +++ b/tests/Moq.Tests/MatchersFixture.cs @@ -80,6 +80,25 @@ public void MatchesIsInEnumerable() Assert.Equal(2, mock.Object.Echo(9)); } + [Fact] + public void MatchesIsInEnumerableWithCustomComparer() + { + var acceptableArgs = Enumerable.Repeat("foo", 1); + var unacceptableArgs = Enumerable.Repeat("bar", 1); + + var mock = new Mock(); + + mock.Setup(x => x.Execute(It.IsIn(acceptableArgs, StringComparer.OrdinalIgnoreCase))).Returns("foo"); + mock.Setup(x => x.Execute(It.IsIn(unacceptableArgs, StringComparer.OrdinalIgnoreCase))).Returns("bar"); + + Assert.Equal("foo", mock.Object.Execute("foo")); + Assert.Equal("foo", mock.Object.Execute("FOO")); + Assert.Equal("foo", mock.Object.Execute("FoO")); + + Assert.Equal("bar", mock.Object.Execute("bar")); + Assert.Equal("bar", mock.Object.Execute("BAR")); + } + [Fact] public void MatchesIsInVariadicParameters() { @@ -112,6 +131,26 @@ public void MatchesIsNotInEnumerable() Assert.Equal(1, mock.Object.Echo(9)); } + [Fact] + public void MatchesIsNotInEnumerableWithCustomComparer() + { + var acceptableArgs = new[] { "foo", "bar" }; + + var mock = new Mock(); + + mock.Setup(x => x.Execute(It.IsNotIn(acceptableArgs, StringComparer.OrdinalIgnoreCase))).Returns("foo"); + + Assert.Equal("foo", mock.Object.Execute("baz")); + Assert.Equal("foo", mock.Object.Execute("alpha")); + + Assert.Equal(default, mock.Object.Execute("foo")); + Assert.Equal(default, mock.Object.Execute("FOO")); + Assert.Equal(default, mock.Object.Execute("FoO")); + Assert.Equal(default, mock.Object.Execute("Bar")); + Assert.Equal(default, mock.Object.Execute("BAR")); + Assert.Equal(default, mock.Object.Execute("bar")); + } + [Fact] public void MatchesIsNotInVariadicParameters() {