存根异步泛型方法

Stubbing an Asynchronous generic methods

我需要在 C# 中存根一个异步泛型方法。 live 方法调用 REST API 并使用 Newtonsoft JSON 将结果反序列化为适当的格式,这将是各种类型的列表。

然而,尝试对其进行存根会遇到各种问题。 下面的主要代码将失败,因为它没有返回任务。

public Task<T> ExportReport<T>(string reportName)
        {
            if(reportName.Contains("personlookup "))
            {
                List<person> people = new();
                people.Add(new person { Forenames = "Bob", Surname = "Brown" });

                return people;
            }
            else if (reportName.Contains("GetOrder"))
                {
                List<order> orders = new();
                orders.Add(new order { ItemType = “Apples”, ID = "1234" });
                return orders;
            }
            else
            {
                return default;
            }
        }

我已经尝试了各种方法,例如 Task.FromResult() 并将方法设置为异步等,到目前为止唯一尝试过甚至可以编译的方法是

return (Task<T>)people.Cast<T>();

它在运行时因无效转换异常而失败。我这里是做错了什么,还是这种方法根本不可能。

了解问题

问题不在于异步泛型函数,而在于一般的泛型函数。您有一个通用函数,它 return 是一个 Task<T> 类型的对象,其中 T 可以是任何类型。 T 的类型由调用者决定,而不是被调用的方法。您正在尝试存根此函数和 return 特定类型。想象一下函数是这样声明的:

public T ExportReport<T>(string reportName)

出于同样的原因,您可能会面临如何对其进行存根的相同问题。

测试与重构

你存根的事实意味着你正在测试(尽管你可能不是)。有时测试会揭示我们代码的设计缺陷。例如,将输入参数(字符串)分支到 return 不同类型的对象的通用方法可能不是此处的最佳解决方案。

我们假设参数 "a" return 是类型 A 的对象,参数 "b" return 是类型 [=18] 的对象=].所以 A myA = ExportReport("a") return 是 A,但是当您键入 A myA = ExportReport("b") 时会发生什么?因为它会编译,你会在某个地方遇到问题。

通过使此函数成为通用函数,您真的有所作为吗?

也许您应该从您的测试经验中得到这个提示并考虑重构?也许您可以有几个不同的非通用函数,每个函数 return 一个不同的报告类型?

讨厌的解决方案(.NET5.0 或更高版本)

如果您不关心这些,只是想让它正常工作,您可以这样做:

public T ExportReport<T>(string reportName)
{
    if (reportName.Contains("personlookup "))
    {
        List<Person> people = new();
        people.Add(new Person { Forenames = "Bob", Surname = "Brown" });

        return Unsafe.As<List<Person>,T>(ref people);
    }

    throw new ArgumentOutOfRangeException(nameof(reportName));
}

和`任务版本:

public Task<T> ExportReport<T>(string reportName)
{
    if (reportName.Contains("personlookup "))
    {
        List<Person> people = new();
        people.Add(new Person { Forenames = "Bob", Surname = "Brown" });
        var peopleTask = Task.FromResult(people);
        return Unsafe.As<Task<List<Person>>,Task<T>>(ref peopleTask);
    }

    throw new ArgumentOutOfRangeException(nameof(reportName));
}

更好的解决方案

听起来您可以使用常规 JSON 转换器序列化您的存根数据并对其进行反消毒,这也解决了问题。

最佳解决方案

可能重构。

来自评论:

It's not for unit testing. I'm just trying to decouple it from an internal API server so I can work on UI elements offline.

考虑到它是为了 有限的目的,那么我想你可以这样做:

// Using `typeof(IEnumerable<T>)` instead of `typeof(List<T>)` or `typeof(IList<T>)` because it means `IsAssignableFrom` will work with `List<T>`, `T[]`, `IReadOnlyList<T>`, `IList<T>`, `ImmutableList<T>`, etc.
// It's maddening that even in .NET 6, `IList<T>` still does not extend `IReadOnlyList<T>`. Grumble.

private static readonly _typeofIEnumerablePerson = typeof(IEnumerable<Person>);
private static readonly _typeofIEnumerableOrder  = typeof(IEnumerable<Order>);

public Task<T> ExportReportAsync<T>(string reportName)
{
    T list = GetStubList<T>();
    return Task.FromResult( list );
}

private static TList GetStubList<TList>()
{
    if( _typeofIEnumerablePerson.IsAssignableFrom( typeof(TList) ) )
    {
        List<Person> people = new List<Person>()
        {
            new Person { Forenames = "Bob", Surname = "Brown" }
        };

        return ToTListWorkaround<TList>( people );
    }
    else if( _typeofIEnumerableOrder.IsAssignableFrom( typeof(TList) ) )
    {
        List<Order> orders = new List<Order>()
        {
            new Order { ItemType = "Apples", ID = "1234" }
        };

        return ToTListWorkaround<TList>( orders );
    }
    else
    {
        throw new NotSupportedException( "Unsupported list type: " + typeof(TList).FullName );
    }
}

private static TList ToTListWorkaround<TList>( IEnumerable actual )
{
    // Ugly workaround:
    Object asObject = actual;
    TList returnValue = (TList)asObject;
    return returnValue;
}