在没有 Mock.Setup() 的情况下调用 Mock.Returns()

Call Mock.Returns() without Mock.Setup()

使用 Moq 时,您经常需要将 mock 配置为 return 特定值。要指定您想要 return 的内容,您必须先完成使用以下结构定义 Setup() 的例程,然后才能定义您的方法应该 return:

var o = new ObjectToReturn();

myMock.Setup(m => m.MyMethod(It.IsAny<T1>()...It.IsAny<Tn>()))
      .Returns(o);

相反,我想做 "For methods with this name, return this value",比如:

myMock.Setup(m => m.GetType().GetMethod("MyMethod")).Returns(o);

myMock.Setup("MyMethod").Return(o);

当我根本不关心类型、值或参数数量时,是否有任何其他方法可以跳过所有方法参数的冗长枚举?

我知道有一个方法 SetReturnsDefault() 但我不想为模拟的所有方法设置默认值。

这是可能的。尝试:

public interface IService
{
    int MyMethod1(int a, object b);

    int MyMethod2(int a);
    int MyMethod2(object b);
}

public static class MyMockExtensions
{
    public static ISetup<T, TResult> Setup<T, TResult>(this Mock<T> mock, 
        string methodName) where T : class
    {
        var methods = typeof(T).GetMethods()
            .Where(
                mi => mi.Name == methodName
                && mi.ReturnType == typeof(TResult))
            .ToArray();

        if (methods.Length == 0)
        {
            throw new MissingMethodException("No method found.");
        }

        if (methods.Length > 1)
        {
            throw new AmbiguousMatchException("Ambiguous methods found.");
        }

        var method = methods[0];

        // Figure out parameters.
        var parameters = method.GetParameters()
            // It.IsAny<pi.ParameterType>()
            .Select(pi => Expression.Call(
                typeof(It), nameof(It.IsAny), new[] { pi.ParameterType }));

        // arg0 => arg0.MyMethod(It.IsAny<T1>()...It.IsAny<Tn>())
        var arg0 = Expression.Parameter(typeof(T), "arg0");
        var setupExpression = Expression.Lambda<Func<T, TResult>>(
            Expression.Call(arg0, method, parameters), arg0);

        return mock.Setup(setupExpression);
    }
}

用法:

var mock = new Mock<IService>();

mock.Setup<IService, int>("MyMethod1").Returns(123);
// method1Result = 123
var method1Result = mock.Object.MyMethod1(1, 2);

// This throws AmbiguousMatchException exception as there are two MyMethod3
// both returning an integer.
mock.Setup<IService, int>("MyMethod2").Returns(234);

挑战在于处理由定义的不同类型的方法 你的模拟类型:

  • 正常方法
  • 通用方法
  • 方法重载

默认值提供程序可能是一个选项。这与 SetReturnsDefault 不同,您有更多的控制权。

A real quick MVP 默认值提供程序如下:

public class SelectiveDefaultValueProvider : DefaultValueProvider
{
    private readonly string _methodName;
    private readonly object _returns;

    public SelectiveDefaultValueProvider(string methodName, object returns)
    {
        _methodName = methodName;
        _returns = returns;
    }

    protected override object GetDefaultValue(Type type, Mock mock)
    {
        var lastInvocation = mock.Invocations.Last();
        var methodInfo = lastInvocation.Method;
        var args = lastInvocation.Arguments;

        if (methodInfo.Name.Equals(_methodName))
        {
            return _returns;
        }

        return type.IsValueType ? Activator.CreateInstance(type) : null;
    }
}

...允许您在值被 returned 之前注入一些决策。在这种情况下,我正在检查最后一个调用方法名称,如果匹配,我将 return 指定的对象 return。我没有使用 args 变量,但我已经包含它以表明您不仅获得了上次调用的 MethodInfo,而且还获得了提供的参数。足以做出明智的决定。

采用以下带有一些重载方法和相同 return 类型的接口:

public class ObjectToReturn
{
    public Guid Id { get; set; }
}

public interface IFoo
{
    ObjectToReturn MyMethod(int parameter1);

    ObjectToReturn MyMethod(string parameter2);

    ObjectToReturn MyMethod(int parameter1, int parameter2);

    ObjectToReturn AnotherMethod();

    int AValueTypeMethod();
}

测试设置看起来像

[Test]
public void DefaultValueProvider_ForOverloadedMethod_AllOverloadsReturnSameExpectedResult()
{
    var objectToReturn = new ObjectToReturn { Id = Guid.NewGuid() };
    var mock = new Mock<IFoo> { DefaultValueProvider = new SelectiveDefaultValueProvider(nameof(IFoo.MyMethod), objectToReturn) };
    var mocked = mock.Object;

    var result1 = mocked.MyMethod(1);
    var result2 = mocked.MyMethod(1, 2);
    var result3 = mocked.MyMethod("asdf");
    var result4 = mocked.AnotherMethod();
    var result5 = mocked.AValueTypeMethod();

    Assert.Multiple(() =>
    {
        Assert.That(result1, Is.SameAs(objectToReturn));
        Assert.That(result2, Is.SameAs(objectToReturn));
        Assert.That(result3, Is.SameAs(objectToReturn));
        Assert.That(result4, Is.Null);
        Assert.That(result5, Is.TypeOf<int>());
    });
}

正如上面提到的 MVP,您可以轻松扩展默认值提供程序实现以获取 methods/return 值的列表,收紧从字符串方法名称到 MethodInfo 结果的接口(typeof(IFoo).GetMethods().Where(x => x.Name.StartsWith("MyMethod") && x.ReturnType == typeof(ObjectToReturn))) 等等。

这种方法的好处是您不必担心构建 Setup 表达式和由此带来的麻烦,如果您确实为成员指定了设置,它将使用该设置;默认值提供程序仅用于没有指定设置的成员。

Working solution