diff --git a/CHANGELOG.md b/CHANGELOG.md index dbbee0881..360a8c3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and `Setup(x => x.GetFooAsync(It.IsAny()).Result).Throws((string s) => n #### Fixed +* The guard against unmatchable matchers (added in #900) was too strict; relaxed it to enable an alternative user-code shorthand `_` for `It.IsAny<>()` (@adamfk, #1199) * mock.Protected() setup methods fail when argument is of type Expression (@tonyhallett, #1189) * Parameter is invalid in Protected().SetupSet() ... VerifySet (@tonyhallett, #1186) diff --git a/src/Moq/MatcherFactory.cs b/src/Moq/MatcherFactory.cs index 62612c348..20d80d0c7 100644 --- a/src/Moq/MatcherFactory.cs +++ b/src/Moq/MatcherFactory.cs @@ -95,14 +95,30 @@ public static Pair CreateMatcher(Expression argument, Para var convertExpression = (UnaryExpression)argument; if (convertExpression.Method?.Name == "op_Implicit") { - if (!parameter.ParameterType.IsAssignableFrom(convertExpression.Operand.Type) && convertExpression.Operand.IsMatch(out _)) + if (convertExpression.Operand.IsMatch(out var match)) { - throw new ArgumentException( - string.Format( - Resources.ArgumentMatcherWillNeverMatch, - convertExpression.Operand.ToStringFixed(), - convertExpression.Operand.Type.GetFormattedName(), - parameter.ParameterType.GetFormattedName())); + Type matchedValuesType; + + if (match.GetType().IsGenericType) + { + // If match type is `Match`, matchedValuesType set to `int` + // Fix for https://github.com/moq/moq4/issues/1199 + matchedValuesType = match.GetType().GenericTypeArguments[0]; + } + else + { + matchedValuesType = convertExpression.Operand.Type; + } + + if (!matchedValuesType.IsAssignableFrom(parameter.ParameterType)) + { + throw new ArgumentException( + string.Format( + Resources.ArgumentMatcherWillNeverMatch, + convertExpression.Operand.ToStringFixed(), + convertExpression.Operand.Type.GetFormattedName(), + parameter.ParameterType.GetFormattedName())); + } } } } diff --git a/tests/Moq.Tests/Matchers/Wildcard.cs b/tests/Moq.Tests/Matchers/Wildcard.cs new file mode 100644 index 000000000..3b8581ddd --- /dev/null +++ b/tests/Moq.Tests/Matchers/Wildcard.cs @@ -0,0 +1,202 @@ +using Xunit; +using System; + +/// +/// Tests for https://github.com/moq/moq4/issues/1199 +/// + +namespace Moq.Tests.Matchers.Wildcard +{ + using static AutoIsAny; // note using static to simplify syntax + + /// + /// Helper class provided by user + /// + public abstract class AutoIsAny + { + public static AnyValue _ + { + get + { + It.IsAny(); + return new AnyValue(); + } + } + } + + /// + /// Helper class provided by user. Interfaces implemented via IDE auto explicit interface implementation + /// or Roslyn analyzer/code fix. + /// + public class AnyValue : ISomeService + { + int ISomeService.Calc(int a, int b, int c, int d) + { + throw new NotImplementedException(); + } + + int ISomeService.DoSomething(ISomeService a, GearId b, int c) + { + throw new NotImplementedException(); + } + + int ISomeService.Echo(int a) + { + throw new NotImplementedException(); + } + + int ISomeService.UseAnimal(Animal a) + { + throw new NotImplementedException(); + } + + int ISomeService.UseDolphin(Dolphin a) + { + throw new NotImplementedException(); + } + + int ISomeService.UseInterface(ISomeService a) + { + throw new NotImplementedException(); + } + + public static implicit operator int(AnyValue _) => default; + public static implicit operator byte(AnyValue _) => default; + public static implicit operator GearId(AnyValue _) => default; + public static implicit operator Animal(AnyValue _) => default; + public static implicit operator Dolphin(AnyValue _) => default; + } + + + public class Tests + { + Mock mock; + ISomeService obj; + + public Tests() + { + mock = new Mock(); + obj = mock.Object; + } + + [Fact] + public void Echo_1Primitive() + { + mock.Setup(obj => obj.Echo(_)).Returns(777); + Assert.Equal(777, obj.Echo(1)); + } + + [Fact] + public void Calc_4Primitives() + { + mock.Setup(obj => obj.Calc(_, _, _, _)).Returns(999); + Assert.Equal(999, obj.Calc(1, 2, 3, 4)); + } + + [Fact] + public void UseInterface() + { + mock.Setup(obj => obj.UseInterface(_)).Returns(555); + Assert.Equal(555, obj.UseInterface(null)); + + var realService = new SomeService(); + Assert.Equal(555, obj.UseInterface(realService)); + } + + [Fact] + public void DoSomething_MixedTypes() + { + mock.Setup(obj => obj.DoSomething(_, _, _)).Returns(444); + Assert.Equal(444, obj.DoSomething(null, GearId.Neutral, 1)); + } + + [Fact] + public void UseAnimal() + { + mock.Setup(obj => obj.UseAnimal(_)).Returns(777); + Assert.Equal(777, obj.UseAnimal(new Animal())); + Assert.Equal(777, obj.UseAnimal(new Dolphin())); + } + + [Fact] + public void UseDolphin() + { + mock.Setup(obj => obj.UseDolphin(_)).Returns(888); + Assert.Equal(888, obj.UseDolphin(new Dolphin())); + } + } + + + /// + /// Example enum + /// + public enum GearId + { + Reverse, + Neutral, + Gear1, + } + + + /// + /// Example interface + /// + public interface ISomeService + { + int Echo(int a); + int Calc(int a, int b, int c, int d); + int UseInterface(ISomeService a); + int DoSomething(ISomeService a, GearId b, int c); + int UseAnimal(Animal a); + int UseDolphin(Dolphin a); + } + + + /// + /// just a class that implements interface + /// + public class SomeService : ISomeService + { + public int Calc(int a, int b, int c, int d) + { + throw new NotImplementedException(); + } + + public int DoSomething(ISomeService a, GearId b, int c) + { + throw new NotImplementedException(); + } + + public int Echo(int a) + { + throw new NotImplementedException(); + } + + public int UseAnimal(Animal a) + { + throw new NotImplementedException(); + } + + public int UseDolphin(Dolphin a) + { + throw new NotImplementedException(); + } + + public int UseInterface(ISomeService a) + { + throw new NotImplementedException(); + } + } + + + public class Animal + { + + } + + + public class Dolphin : Animal + { + + } +}