Skip to content

Commit

Permalink
Implement It.Is, It.IsIn, It.IsNotIn with a comparer overload (#1059)
Browse files Browse the repository at this point in the history
  • Loading branch information
weitzhandler committed Sep 30, 2020
1 parent bd4787f commit 0cc2433
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 0 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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<T>` (@weitzhandler, #1064)


## 4.14.5 (2020-07-01)

#### Fixed
Expand Down
41 changes: 41 additions & 0 deletions src/Moq/It.cs
Expand Up @@ -175,6 +175,25 @@ public static TValue Is<TValue>(Expression<Func<object, Type, bool>> match)
Expression.Lambda<Func<TValue>>(Expression.Call(thisMethod.MakeGenericMethod(typeof(TValue)), match)));
}

/// <summary>
/// Matches any value that equals the <paramref name="value"/> using the <paramref name="comparer"/>.
/// To use the default comparer for the specified object, specify the value inline,
/// i.e. <code>mock.Verify(service => service.DoWork(value))</code>.
/// <para>
/// Use this overload when you specify a value and a comparer.
/// </para>
/// </summary>
/// <param name="value">The value to match with.</param>
/// <param name="comparer">An <see cref="IEqualityComparer{T}"/> with which the values should be compared.</param>
/// <typeparam name="TValue">Type of the argument to check.</typeparam>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static TValue Is<TValue>(TValue value, IEqualityComparer<TValue> comparer)
{
var thisMethod = (MethodInfo)MethodBase.GetCurrentMethod();

return Match.Create<TValue>(actual => comparer.Equals(actual, value), () => It.Is<TValue>(value, comparer));
}

/// <summary>
/// Matches any value that is in the range specified.
/// </summary>
Expand Down Expand Up @@ -234,6 +253,17 @@ public static TValue IsIn<TValue>(IEnumerable<TValue> items)
return Match<TValue>.Create(value => items.Contains(value), () => It.IsIn(items));
}

/// <summary>
/// Matches any value that is present in the sequence specified.
/// </summary>
/// <param name="items">The sequence of possible values.</param>
/// <param name="comparer">An <see cref="IEqualityComparer{T}"/> with which the values should be compared.</param>
/// <typeparam name="TValue">Type of the argument to check.</typeparam>
public static TValue IsIn<TValue>(IEnumerable<TValue> items, IEqualityComparer<TValue> comparer)
{
return Match<TValue>.Create(value => items.Contains(value, comparer), () => It.IsIn(items, comparer));
}

/// <summary>
/// Matches any value that is present in the sequence specified.
/// </summary>
Expand Down Expand Up @@ -276,6 +306,17 @@ public static TValue IsNotIn<TValue>(IEnumerable<TValue> items)
return Match<TValue>.Create(value => !items.Contains(value), () => It.IsNotIn(items));
}

/// <summary>
/// Matches any value that is not found in the sequence specified.
/// </summary>
/// <param name="items">The sequence of disallowed values.</param>
/// <param name="comparer">An <see cref="IEqualityComparer{T}"/> with which the values should be compared.</param>
/// <typeparam name="TValue">Type of the argument to check.</typeparam>
public static TValue IsNotIn<TValue>(IEnumerable<TValue> items, IEqualityComparer<TValue> comparer)
{
return Match<TValue>.Create(value => !items.Contains(value, comparer), () => It.IsNotIn(items, comparer));
}

/// <summary>
/// Matches any value that is not found in the sequence specified.
/// </summary>
Expand Down
61 changes: 61 additions & 0 deletions tests/Moq.Tests/CustomTypeMatchersFixture.cs
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IX>();
mock.Setup(m => m.Method(It.Is<string>(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<IX>();
mock.Setup(m => m.Method(It.Is<object>(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<T>();
Expand Down Expand Up @@ -229,5 +271,24 @@ public struct AnyStruct : ITypeMatcher
{
public bool Matches(Type typeArgument) => typeArgument.IsValueType;
}

public class ObjectStringOrdinalIgnoreCaseComparer : IEqualityComparer<object>
{
private static IEqualityComparer<string> 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);
}
}
}
}
39 changes: 39 additions & 0 deletions tests/Moq.Tests/MatchersFixture.cs
Expand Up @@ -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<IFoo>();

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()
{
Expand Down Expand Up @@ -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<IFoo>();

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()
{
Expand Down

0 comments on commit 0cc2433

Please sign in to comment.