存根异步泛型方法
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;
}
我需要在 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;
}