Stubbing一个异步泛型方法



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

然而,试图将其截断会遇到各种问题。下面的主代码将失败,因为它没有返回Task。

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((并将该方法设置为async等,到目前为止,唯一尝试过的甚至可以编译的是

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

它在运行时失败,出现无效的强制转换异常。我是不是做错了什么,或者这种方法是不可能的。

了解问题

问题不在于异步泛型函数,而在于一般的泛型函数。您有一个返回类型为Task<T>的对象的泛型函数,其中T可以是任何类型。T的类型由调用者决定,而不是由被调用的方法决定。您正试图截断此函数并返回一个特定的类型。想象一下,函数是这样声明的:

public T ExportReport<T>(string reportName)

由于同样的原因,你也会面临同样的问题,即如何存根。

测试&重构

事实上,你正在存根意味着你正在测试(尽管你可能不是(。有时测试会发现我们的代码存在设计缺陷。例如,在输入参数(字符串(上分支以返回不同类型对象的泛型方法可能不是最佳解决方案。

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

通过使这个函数通用,你真的取得了什么成就吗?

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

Nasty解决方案(.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转换器序列化存根数据并对其进行去验证,这也解决了问题。

最佳解决方案

可能是重构。

来自注释:

它不用于单元测试。我只是试图将它与内部API服务器解耦,这样我就可以脱机处理UI元素。

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

// 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;
}

相关内容

最新更新