首页 > 解决方案 > 是否可以创建一个将方法组作为参数的通用方法

问题描述

我正在尝试创建一个允许传入“方法组”的方法。基本上这是为了测试我想确保调用方法的地方。FakeItEasy 已经允许以下代码

public static IAnyCallConfigurationWithReturnTypeSpecified<ReturnType> ReturnForMethod<SUT, ReturnType>(
          this Fake<SUT> fake,
           string methodName, ReturnType returnObj)
      {
          var call = fake.AnyCall().WithReturnType<ReturnType>().Where(s => s.Method.Name == methodName);

          call.Returns(returnObj);
          return call;
      }

//Called using 
new Fake<IDummyFactory>().ReturnForMethod(nameof(IDummyFactory.Create), testStr);

为了更简洁的代码,我更喜欢写类似的东西

public static IAnyCallConfigurationWithReturnTypeSpecified<ReturnType> ReturnForMethodF<SUT ,ReturnType>(
            this Fake<SUT> fake,
            MethodGroup method, ReturnType returnObj)

nameof 的工作方式基本相同。使用一个函数,所以我希望调用如下的方法

new Fake<IDummyFactory>().ReturnForMethod(s=> s.Create, testStr); --NoInvoke as just want method group

标签: c#fakeiteasy

解决方案


“方法组”是一个只存在于语言语法中的概念,没有对应于方法组的运行时类型。

在我回答问题之前,您可以在 callsite 执行此操作:

new Fake<IDummyFactory>().ReturnForMethod(nameof(s.Create), testStr);

我认为没有任何理由s => s.Createnameof(s.Create).

现在是有趣的部分。方法组可转换为兼容的委托类型,由一组极其复杂的魔术规则管理

因此,我们将寻找具有以下签名的方法:

public static IAnyCallConfigurationWithReturnTypeSpecified<TReturnType> ReturnForMethod<TSUT, TDelegate, TReturnType>(
    this Fake<TSUT> fake,
    Expression<Func<TSUT, TDelegate>> methodGroupExpression,
    TReturnType returnObject) where TSUT : class 
                              where TDelegate : Delegate

我们正在使用Expressions,因此我们必须创建一个ExpressionVisitor遍历Expression树并查找适当类型的方法调用的方法。为简单起见,我们将假设您始终只使用一个x => x.Foo表达式调用该方法。假设我们有一个执行此操作的访问者类,我们方法的主体很简单:

public static IAnyCallConfigurationWithReturnTypeSpecified<TReturnType> ReturnForMethod<TSUT, TDelegate, TReturnType>(
    this Fake<TSUT> fake,
    Expression<Func<TSUT, TDelegate>> methodGroupExpression,
    TReturnType returnObject) where TSUT : class 
                                where TDelegate : Delegate
{
    var visitor = new ExtractMethodNameExpressionVisitor<TSUT>();
    var methodName = visitor.ExtractMethodName(methodGroupExpression);

    if (methodName is null)
    {
        throw new InvalidOperationException();
    }

    var call = fake.AnyCall().WithReturnType<TReturnType>().Where(s => s.Method.Name == methodName);

    call.Returns(returnObject);
    return call;
}

该表达式s => s.Create在编译期间通过一些神奇的编译器生成的代码进行了丰富,这些代码将其转换为适当的委托类型。但是方法组本身作为ConstantExpressiontype嵌套在其中的某个地方MethodInfo。我们的访问者类将遍历树,找到这样的表达式并保存它的名称。

class ExtractMethodNameExpressionVisitor<T> : ExpressionVisitor
{
    private string? _methodName;

    public string? ExtractMethodName(Expression expression)
    {
        _methodName = null;

        this.Visit(expression);
        return _methodName;
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        if (node.Type == typeof(MethodInfo) && 
            node.Value is MethodInfo methodInfo &&
            methodInfo.DeclaringType == typeof(T))
        {
            _methodName = methodInfo.Name;
        }

        return base.VisitConstant(node);
    }
}

现在我们可以测试一下:

var fake = new Fake<IDummyFactory>();

fake.ReturnForMethod(s => s.Create, "testStr");

var result = fake.FakedObject.Create();

Console.WriteLine(result);

不幸的是,这不起作用。至少从 C#9 开始,委托类型在语言中是一种二等公民,对它们的泛型类型推断不起作用。所以上面的ReturnForMethod调用不会编译,因为编译器无法推断出一个返回string且不带参数的函数实际上是Func<string>. 所以调用站点必须如下所示:

fake.ReturnForMethod<IDummyFactory, Func<string>, string>(s => s.Create, "testStr");

这使得该解决方案的吸引力大大降低。好消息是,如果我正确理解了这些建议,那么在 C#10(或者如果该功能错过 C#10 的发布窗口,则可能是 C#11)这将在没有显式类型签名的情况下工作。

您可以在此处找到完整的工作演示。


推荐阅读