admin管理员组

文章数量:1355528

Here is my code, as you can see there are many It.IsAny<> which look ugly.

    _mockTimeSpanModel
    .Setup(
        x => x.CaptureTargetScreen(
            It.IsAny<string>(),
            It.IsAny<int>(),
            It.IsAny<int>(),
            It.IsAny<Action<Bitmap>>()
        )
    )
    .Callback(
        (string windowTitle, int offsetFromLeft, int offsetFromBottom, Action<Bitmap> onImageCaptured) =>
        {
            if( windowTitle == "my" ) ...
        }
    );

The code I want is as below. Instead of It.IsAny<>, default values are used. It looks clean. But it does not work as above.

    _mockTimeSpanModel
    .Setup(
        x => x.CaptureTargetScreen(
            "",
            0,
            0,
            null
        )
    )
    .Callback(
        (string windowTitle, int offsetFromLeft, int offsetFromBottom, Action<Bitmap> onImageCaptured) =>
        {
            if( windowTitle == "my" ) ...
        }
    );

Can I make it work as the first code without using It.IsAny<T>?

Here is my code, as you can see there are many It.IsAny<> which look ugly.

    _mockTimeSpanModel
    .Setup(
        x => x.CaptureTargetScreen(
            It.IsAny<string>(),
            It.IsAny<int>(),
            It.IsAny<int>(),
            It.IsAny<Action<Bitmap>>()
        )
    )
    .Callback(
        (string windowTitle, int offsetFromLeft, int offsetFromBottom, Action<Bitmap> onImageCaptured) =>
        {
            if( windowTitle == "my" ) ...
        }
    );

The code I want is as below. Instead of It.IsAny<>, default values are used. It looks clean. But it does not work as above.

    _mockTimeSpanModel
    .Setup(
        x => x.CaptureTargetScreen(
            "",
            0,
            0,
            null
        )
    )
    .Callback(
        (string windowTitle, int offsetFromLeft, int offsetFromBottom, Action<Bitmap> onImageCaptured) =>
        {
            if( windowTitle == "my" ) ...
        }
    );

Can I make it work as the first code without using It.IsAny<T>?

Share Improve this question edited Mar 31 at 15:44 Michał Turczyn 37.8k17 gold badges54 silver badges82 bronze badges asked Mar 31 at 14:10 ZachZach 5,92312 gold badges50 silver badges66 bronze badges 6
  • 3 It's not clean if it doesn't work. "" or 0 are very specific values. They can't be used as wildcards. You need a syntax that says "use any value" – Panagiotis Kanavos Commented Mar 31 at 14:32
  • So the only way is to hope future MOQ will add a new Setup2 function to treat those default values as It.IsAny<> . I just checked the source code, but it is too complicated for me to add a new Setup2 function. @PanagiotisKanavos – Zach Commented Mar 31 at 15:22
  • I said the exact opposite, and I hope Moq never does this. Default values are very specific values that should NEVER be treated as wildcards. If I pass 0 to a Multiply mock I ALWAYS expect the result to be 0. What would make sense would be an It.IsAny() that doesn't require a type. This can't work right now because C# can't infer type arguments from return values. You can probably write using static It; and use IsAny<string>(). – Panagiotis Kanavos Commented Apr 1 at 7:06
  • @PanagiotisKanavos If Default values are really needed, can be done by using It.Is<int>(t=>t==0) – Zach Commented Apr 1 at 8:47
  • That's an argument for the use of It.IsAny<int>(). If special syntax is needed, it should be used for special cases. Default values aren't special values. They're normal values and everyone expects them to be the same in every .NET application or library. Point p=default is expected to be the same as new Point() or new Point(0,0). And a string s=default; is expected to be null, not "". – Panagiotis Kanavos Commented Apr 1 at 9:04
 |  Show 1 more comment

3 Answers 3

Reset to default 4

Treating the default values as any would be problematic because sometimes you would want to use the default values for their values, not as token for any, i.e. 0 as zero.

If you don't like the verbosity there are several "workarounds".

One is defining a static helper class for common types like int, string, etc.:

public static class Any {
    public static int Int => It.IsAny<int>();
}

This would save you a bit of typing Any.Int instead of It.IsAny<int>().

or alternatively if you'd prefer not typing the paranthesis but still have it generic and somewhat fluent Any<int>.Value over It.IsAny<int>():

public static class Any<T> {
    public static T Value => It.IsAny<T>();
}

Door number 3 involves a bit of work with Expressions, but will give you exactly what you want. When you put default for an argument it will be replaced with It.IsAny<T>(): dotnet fiddle

public static class Extensions {

    public static ISetup<T> SetupWithAny<T>(this Mock<T> mock, Expression<Action<T>> action)
    where T : class {
        var expVisitor = new ItIsAnyExpressionVisitor();
        var itIsAnyExpression = (Expression<Action<T>>)expVisitor.Visit(action);
        return mock.Setup(itIsAnyExpression);
    }

    public static ISetup<T, TReturn> SetupWithAny<T, TReturn>(this Mock<T> mock,
    Expression<Func<T, TReturn>> func)
where T : class {
        var expVisitor = new ItIsAnyExpressionVisitor();
        var itIsAnyExpression = (Expression<Func<T, TReturn>>)expVisitor.Visit(func);
        return mock.Setup(itIsAnyExpression);
    }


    private class ItIsAnyExpressionVisitor : ExpressionVisitor {
        static MethodInfo s_IsAnyMethodInfo = typeof(It)
            .GetMethods()
            .Where(x => x.Name.Contains("IsAny") && x.IsGenericMethod)
            .FirstOrDefault();

        static ConcurrentDictionary<Type, MethodInfo>
        s_genericMethodInfos = new ConcurrentDictionary<Type, MethodInfo>();

        
        #region DefaultValue
        private static MethodInfo s_getDefaultTypeGeneric =
            typeof(ItIsAnyExpressionVisitor).GetMethod(nameof(ItIsAnyExpressionVisitor.GetDefaultTypeValueCore),
             BindingFlags.Static | BindingFlags.NonPublic);
        private static ConcurrentDictionary<Type, object> s_defaultValues = new();
        private static T GetDefaultTypeValueCore<T>() => default(T);
        private static object GetDefaultTypeValue(Type type) {
            ArgumentNullException.ThrowIfNull(type);
            if (!type.IsValueType) return null;
            var defaultValue = s_defaultValues.GetOrAdd(type,
                t => s_getDefaultTypeGeneric.MakeGenericMethod(t).Invoke(null, null));
            return defaultValue;
        }
        #endregion
        
        protected override Expression VisitMethodCall(MethodCallExpression node) {
            var itIsAnyArguments = node.Arguments
            .Select(a => {
                if (a is not ConstantExpression constExpr)
                    return a;

                var defaultValueForType = GetDefaultTypeValue(a.Type);

                // default(T) is null and Value is not null -> we take it
                if (defaultValueForType == null 
                    && constExpr.Value != null)
                    return a;

                // default(T) is not null, i.e. 0
                // so if it's not Equal, i.e. 10 then return
                if (defaultValueForType != null
                && defaultValueForType.Equals(constExpr.Value) == false)
                    return a;

                return Expression.Call(null,
                s_genericMethodInfos.GetOrAdd(a.Type,
                t => s_IsAnyMethodInfo.MakeGenericMethod(t)));
            })
            .ToArray();


            var newMethodCall = Expression.Call(
            node.Object, node.Method, itIsAnyArguments);
            return newMethodCall;
        }
    }
}

Test code:

public interface Foo {
    void Bar(int a, string b);
    int Baz(string a);
}

var mock = new Mock<Foo>();
//mock.Setup(x => x.Bar(42, default))
//.Callback(() => Console.WriteLine("CALLED from Regular Setup"));

mock.SetupWithAny(x => x.Bar(42, default)) // change to 43 and won't "work"
.Callback(() => Console.WriteLine("CALLED from Extension"));

//mock.Setup(x => x.Baz(default)).Returns(4);
mock.SetupWithAny(x => x.Baz(default)).Returns(4444);
mock.Object.Bar(42, "baz");

// CALLED from Extension
Console.WriteLine(mock.Object.Baz("foo")); // 4444

According to general logic, it's really hard to give an authoritative negative answer, but with a decade or two of experience with .NET and Moq, I'm fairly convinced that the answer is no.

The suggested alternative ought to compile, but of course, this will only match if the first argument is the empty string, the two next ones 0, and the last null.

Consider that this combination may be a a valid combination that you'd like to model in a test case. The Moq library can't tell that that's not what you mean.

If you don't like repeating those It.IsAny<T> calls, consider writing a Test Helper that encapsulates this setup. Something like

internal static ?? SetupCaptureTargetScreenAny(this Mock<TimeSpanModel> mockTimeSpanModel)
{
    mockTimeSpanModel.Setup(
        x => x.CaptureTargetScreen(
            It.IsAny<string>(),
            It.IsAny<int>(),
            It.IsAny<int>(),
            It.IsAny<Action<Bitmap>>()
        )
    )
}

I can't quite remember what type Setup returns, so I've just indicated ??, but you should be able to look that up.

All that said, I'd like to warn against relying too much on It.IsAny. It can be fine when testing certain error paths, but in general, the test should specify the arguments that the dependency expects. Otherwise, you'll risk that your tests behave in all sorts of unexpected ways. Ask my how I know.

Only thing that comes to my mind to avoid repeating It.IsAny over and over is to use little bit of reflection. Eventually what I aim is to pass minimal information, i.e. mock, method name and return value for mocked method:

public static class MockHelper
{
    private static MethodInfo GetMethod<TMock>(string methodName) where TMock : class
    {
        var method = typeof(TMock)
            .GetMethods(BindingFlags.Public | BindingFlags.Instance)
            .FirstOrDefault(m => m.Name == methodName);

        if (method == null)
        {
            throw new ArgumentException($"Method {methodName} not found in {typeof(TMock).Name}.");
        }

        return method;
    }

    private static Expression[] GetParameterExpressions(MethodInfo method)
    {
        return method.GetParameters()
            .Select(p => Expression.Call(typeof(It), nameof(It.IsAny), new Type[] { p.ParameterType }))
            .ToArray();
    }

    public static void SetupMethodWithAny<TMock, TReturn>(
        this Mock<TMock> mock,
        string methodName,
        TReturn returnValue) where TMock : class
    {
        var method = GetMethod<TMock>(methodName);
        var mockParameter = Expression.Parameter(typeof(TMock), "m");
        var methodCall = Expression.Call(mockParameter, method, GetParameterExpressions(method));
        var lambda = Expression.Lambda<Func<TMock, TReturn>>(methodCall, mockParameter);

        var setupMethod = typeof(Mock<TMock>)
            .GetMethods()
            .FirstOrDefault(m => m.Name == nameof(Mock<TMock>.Setup) && m.IsGenericMethod)
            ?.MakeGenericMethod(typeof(TReturn));

        var setup = setupMethod?.Invoke(mock, new object[] { lambda });
        (setup as Moq.Language.Flow.ISetup<TMock, TReturn>)?.Returns(returnValue);
    }
}

Code basically is using reflection to find all relevant method such as It.IsAny and Setup. Then uses passed mock to wire everything up.

Possible vectors for improvments:

  • making overload that would mock void methods,
  • extend to accept factory method instead of returnValue

EDIT

Here's sample implementation for void methods, that would throw an exception:

public static void SetupMethodWithException<TMock, TException>(
    this Mock<TMock> mock,
    string methodName,
    TException exception)
    where TException : Exception
    where TMock : class
{
    var method = GetMethod<TMock>(methodName);
    var mockParameter = Expression.Parameter(typeof(TMock), "m");
    var methodCall = Expression.Call(mockParameter, method, GetParameterExpressions(method));
    var lambda = Expression.Lambda<Action<TMock>>(methodCall, mockParameter);

    var setupMethod = typeof(Mock<TMock>)
        .GetMethods()
        .FirstOrDefault(m => m.Name == nameof(Mock<TMock>.Setup) && !m.IsGenericMethod);

    var setup = setupMethod?.Invoke(mock, new object[] { lambda });
    (setup as Moq.Language.Flow.ISetup<TMock>)?.Throws(exception);
}

Examples

Having such setup:

public interface ISystemUnderTest
{
    void Test();

    string ReturnTest(string a, int b);
}

Here are example unit tests with the methods:

public class UnitTest1
{
    [Fact]
    public void Test()
    {
        // Arrange
        var mock = new Mock<ISystemUnderTest>();
        mock.SetupMethodWithException(nameof(ISystemUnderTest.Test), new InvalidOperationException("My Exception"));

        // Act & Assert
        Assert.Throws<InvalidOperationException>(mock.Object.Test);
    }

    [Fact]
    public void ReturnTest()
    {
        // Arrange
        var mock = new Mock<ISystemUnderTest>();
        var guid = Guid.NewGuid().ToString();
        mock.SetupMethodWithAny(nameof(ISystemUnderTest.ReturnTest), guid);

        // Act
        var result = mock.Object.ReturnTest();

        // Assert
        Assert.Equal(guid, result);
    }
}

本文标签: cHow to match any parameters in Setup() without using ItIsAnyltTgtStack Overflow