mirror of
https://gitee.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git
synced 2025-09-19 18:22:24 +08:00
test: 提取公共测试流程
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat\SKIT.FlurlHttpClient.Wechat.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
316
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestAssertUtil.cs
Normal file
316
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestAssertUtil.cs
Normal file
@@ -0,0 +1,316 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat
|
||||
{
|
||||
public static class TestAssertUtil
|
||||
{
|
||||
private static bool TryJsonize(string json, Type type, out Exception exception)
|
||||
{
|
||||
exception = null;
|
||||
|
||||
var newtonsoftJsonSettings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
|
||||
newtonsoftJsonSettings.CheckAdditionalContent = true;
|
||||
newtonsoftJsonSettings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
|
||||
var newtonsoftJsonSerializer = new FlurlNewtonsoftJsonSerializer(newtonsoftJsonSettings);
|
||||
var systemTextJsonSerializer = new FlurlSystemTextJsonSerializer();
|
||||
|
||||
try
|
||||
{
|
||||
newtonsoftJsonSerializer.Deserialize(json, type);
|
||||
systemTextJsonSerializer.Deserialize(json, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is Newtonsoft.Json.JsonException)
|
||||
exception = new Exception($"通过 Newtonsoft.Json 反序列化 `{type.Name}` 失败。", ex);
|
||||
else if (ex is System.Text.Json.JsonException)
|
||||
exception = new Exception($"通过 System.Text.Json 反序列化 `{type.Name}` 失败。", ex);
|
||||
else
|
||||
exception = new Exception($"JSON 反序列化 `{type.Name}` 遇到问题。", ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object instance = Activator.CreateInstance(type);
|
||||
TestReflectionUtil.InitializeProperties(instance);
|
||||
|
||||
newtonsoftJsonSerializer.Serialize(instance, type);
|
||||
systemTextJsonSerializer.Serialize(instance, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is Newtonsoft.Json.JsonException)
|
||||
exception = new Exception($"通过 Newtonsoft.Json 序列化 `{type.Name}` 失败。", ex);
|
||||
else if (ex is System.Text.Json.JsonException)
|
||||
exception = new Exception($"通过 System.Text.Json 序列化 `{type.Name}` 失败。", ex);
|
||||
else
|
||||
exception = new Exception($"JSON 序列化 `{type.Name}` 遇到问题。", ex);
|
||||
}
|
||||
|
||||
PropertyInfo[] lstPropInfo = TestReflectionUtil.GetAllProperties(type);
|
||||
foreach (PropertyInfo propInfo in lstPropInfo)
|
||||
{
|
||||
var newtonsoftJsonAttribute = propInfo.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>();
|
||||
var systemTextJsonAttribute = propInfo.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
|
||||
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
|
||||
exception = new Exception($"类型 `{type.Name}` 的可 JSON 序列化字段声明不一致:`{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`。");
|
||||
}
|
||||
|
||||
return exception == null;
|
||||
}
|
||||
|
||||
public static bool VerifyApiModelsNaming(Assembly assembly, out Exception exception)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
|
||||
var lstError = new List<Exception>();
|
||||
|
||||
foreach (Type modelType in lstModelType)
|
||||
{
|
||||
string name = modelType.Name.Split('`')[0];
|
||||
|
||||
if (!name.EndsWith("Request") && !name.EndsWith("Response"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 类名结尾应为 \"Request\" 或 \"eponse\"。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name.EndsWith("Request"))
|
||||
{
|
||||
if (!typeof(IWechatRequest).IsAssignableFrom(modelType))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 类需实现自 `IWechatRequest`。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModelType.Any(e => e.Name == $"{name.Substring(0, name.Length - "Request".Length)}Response"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 是请求模型,但不存在对应的响应模型。"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (name.EndsWith("Response"))
|
||||
{
|
||||
if (!typeof(IWechatResponse).IsAssignableFrom(modelType))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 类需实现自 `IWechatResponse`。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModelType.Any(e => e.Name == $"{name.Substring(0, name.Length - "Response".Length)}Request"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 是响应模型,但不存在对应的请求模型。"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lstError.Any())
|
||||
{
|
||||
exception = new AggregateException(lstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool VerifyApiModelsDefinition(Assembly assembly, string workdir, out Exception exception)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
if (workdir == null) throw new ArgumentNullException(nameof(workdir));
|
||||
|
||||
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
|
||||
var lstError = new List<Exception>();
|
||||
|
||||
var lstFile = TestIOUtil.GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
if (!lstFile.Any())
|
||||
{
|
||||
lstError.Add(new Exception($"路径 \"{workdir}\" 下不存在 JSON 格式的模型示例文件,请检查路径是否正确。"));
|
||||
}
|
||||
|
||||
foreach (string file in lstFile)
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = assembly.GetType($"{assembly.GetName().Name}.Models.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
lstError.Add(new Exception($"类型 `{name}`不存在。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryJsonize(json, type, out Exception ex))
|
||||
{
|
||||
lstError.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (lstError.Any())
|
||||
{
|
||||
exception = new AggregateException(lstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool VerifyApiEventsDefinition(Assembly assembly, string workdir, out Exception exception)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
if (workdir == null) throw new ArgumentNullException(nameof(workdir));
|
||||
|
||||
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
|
||||
var lstError = new List<Exception>();
|
||||
|
||||
var lstJsonFile = TestIOUtil.GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
var lstXmlFile = TestIOUtil.GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".xml", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
if (!lstJsonFile.Any() && !lstXmlFile.Any())
|
||||
{
|
||||
lstError.Add(new Exception($"路径 \"{workdir}\" 下不存在 JSON 或 XML 格式的事件示例文件,请检查路径是否正确。"));
|
||||
}
|
||||
|
||||
foreach (string file in lstJsonFile)
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = assembly.GetType($"{assembly.GetName().Name}.Events.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
lstError.Add(new Exception($"类型 `{name}`不存在。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryJsonize(json, type, out Exception ex))
|
||||
{
|
||||
lstError.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string file in lstXmlFile)
|
||||
{
|
||||
string xml = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = assembly.GetType($"{assembly.GetName().Name}.Events.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
lstError.Add(new Exception($"类型 `{name}`不存在。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using StringReader reader = new StringReader(xml);
|
||||
XmlSerializer xmlSerializer = new XmlSerializer(type, new XmlRootAttribute("xml"));
|
||||
xmlSerializer.Deserialize(reader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = new Exception($"XML 反序列化 `{type.Name}` 遇到问题。", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (lstError.Any())
|
||||
{
|
||||
exception = new AggregateException(lstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool VerifyApiExtensionsNaming(Assembly assembly, out Exception exception)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
var lstExtType = TestReflectionUtil.GetAllApiExtensionsTypes(assembly);
|
||||
var lstError = new List<Exception>();
|
||||
|
||||
foreach (Type extType in lstExtType)
|
||||
{
|
||||
MethodInfo[] lstMethod = extType.GetMethods()
|
||||
.Where(e =>
|
||||
e.IsPublic &&
|
||||
e.IsStatic &&
|
||||
typeof(IWechatClient).IsAssignableFrom(e.GetParameters().FirstOrDefault().ParameterType)
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
foreach (MethodInfo methodInfo in lstMethod)
|
||||
{
|
||||
ParameterInfo[] lstParamInfo = methodInfo.GetParameters();
|
||||
|
||||
// 参数签名必为 this client + request + cancelToken
|
||||
if (lstParamInfo.Length != 3)
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法需有且仅有 3 个入参。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 第二个参数必为 IWechatRequest 子类
|
||||
if (!typeof(IWechatRequest).IsAssignableFrom(lstParamInfo[1].ParameterType))
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法第 1 个入参需实现自 `IWechatRequest`。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 方法名与第二个参数、返回值均有相同命名
|
||||
string func = methodInfo.Name;
|
||||
string para = lstParamInfo[1].ParameterType.Name;
|
||||
string retv = methodInfo.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
|
||||
if (para == null || !para.EndsWith("Request"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法第 1 个入参类名应以 `Request` 结尾。"));
|
||||
continue;
|
||||
}
|
||||
else if (retv == null || !retv.EndsWith("Response"))
|
||||
{
|
||||
if (!methodInfo.ReturnType.GenericTypeArguments.First().IsGenericType)
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法返回值类名应以 `Response` 结尾。"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法与请求模型应同名。"));
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法与响应模型应同名。"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lstError.Any())
|
||||
{
|
||||
exception = new AggregateException(lstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
27
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestIOUtil.cs
Normal file
27
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestIOUtil.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat
|
||||
{
|
||||
public static class TestIOUtil
|
||||
{
|
||||
public static string[] GetAllFiles(string path)
|
||||
{
|
||||
if (path == null) throw new ArgumentNullException(nameof(path));
|
||||
|
||||
List<string> results = new List<string>();
|
||||
string[] dirs = Directory.GetDirectories(path);
|
||||
string[] files = Directory.GetFiles(path);
|
||||
|
||||
results.AddRange(files);
|
||||
|
||||
foreach (string dir in dirs)
|
||||
{
|
||||
results.AddRange(GetAllFiles(dir));
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
158
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestReflectionUtil.cs
Normal file
158
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestReflectionUtil.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat
|
||||
{
|
||||
public static class TestReflectionUtil
|
||||
{
|
||||
public static Type[] GetAllApiModelsTypes(Assembly assembly)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
return assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(assembly.GetName().Name + ".Models") &&
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface &&
|
||||
!e.IsNested
|
||||
)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static Type[] GetAllApiExtensionsTypes(Assembly assembly)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
return assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(assembly.GetName().Name) &&
|
||||
e.Name.StartsWith("Wechat") &&
|
||||
e.Name.Contains("ClientExecute") &&
|
||||
e.Name.EndsWith("Extensions")
|
||||
)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static Type[] GetAllApiEventsTypes(Assembly assembly)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
return assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(assembly.GetName().Name + ".Events") &&
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface &&
|
||||
!e.IsNested
|
||||
)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static PropertyInfo[] GetAllProperties(Type type)
|
||||
{
|
||||
if (type == null) throw new ArgumentNullException(nameof(type));
|
||||
|
||||
var lstProperty = type.GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
|
||||
|
||||
type.GetNestedTypes()
|
||||
.Where(e =>
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface
|
||||
)
|
||||
.ToList()
|
||||
.ForEach(e =>
|
||||
{
|
||||
lstProperty.AddRange(GetAllProperties(e));
|
||||
});
|
||||
|
||||
return lstProperty.Distinct().ToArray();
|
||||
}
|
||||
|
||||
public static object InitializeProperties(object obj)
|
||||
{
|
||||
const int MAX_DEPTH = 10; // 防止无限递归
|
||||
int CUR_DEPTH = 0;
|
||||
|
||||
Func<object, object> func = null;
|
||||
func = new Func<object, object>((obj) =>
|
||||
{
|
||||
CUR_DEPTH++;
|
||||
|
||||
if (CUR_DEPTH >= MAX_DEPTH)
|
||||
return obj;
|
||||
|
||||
PropertyInfo[] lstPropInfo = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (PropertyInfo propInfo in lstPropInfo)
|
||||
{
|
||||
if (propInfo.SetMethod == null || !propInfo.SetMethod.IsPublic)
|
||||
continue;
|
||||
|
||||
if (propInfo.PropertyType.IsPrimitive)
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (propInfo.PropertyType.IsArray)
|
||||
{
|
||||
Type elType = propInfo.PropertyType.Assembly.GetType(propInfo.PropertyType.FullName.Replace("[]", string.Empty));
|
||||
object elObj = (elType == typeof(string)) ? string.Empty : Activator.CreateInstance(elType);
|
||||
elObj = Convert.ChangeType(elObj, elType);
|
||||
func(elObj);
|
||||
|
||||
Array prop = Array.CreateInstance(elType, 1);
|
||||
prop.SetValue(elObj, 0);
|
||||
|
||||
propInfo.SetValue(obj, prop);
|
||||
}
|
||||
else if (propInfo.PropertyType == typeof(string))
|
||||
{
|
||||
propInfo.SetValue(obj, string.Empty);
|
||||
}
|
||||
else if (propInfo.PropertyType.Namespace == "System" &&
|
||||
propInfo.PropertyType.Name.StartsWith("Nullable"))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (propInfo.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(propInfo.PropertyType.Name.StartsWith("IDictionary") || propInfo.PropertyType.Name.StartsWith("Dictionary")))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (propInfo.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(propInfo.PropertyType.Name.StartsWith("IList") || propInfo.PropertyType.Name.StartsWith("List")))
|
||||
{
|
||||
Type elElementType = propInfo.PropertyType.GetGenericArguments().Single();
|
||||
object elElementObj = (elElementType == typeof(string)) ? string.Empty : Activator.CreateInstance(elElementType);
|
||||
elElementObj = Convert.ChangeType(elElementObj, elElementType);
|
||||
func(elElementObj);
|
||||
|
||||
Type elListType = typeof(List<>).MakeGenericType(new Type[] { elElementType });
|
||||
object elListObj = Activator.CreateInstance(elListType);
|
||||
elListType.GetMethod("Add").Invoke(elListObj, new[] { elElementObj });
|
||||
|
||||
propInfo.SetValue(obj, elListObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
object elObj = Activator.CreateInstance(propInfo.PropertyType);
|
||||
func(elObj);
|
||||
|
||||
propInfo.SetValue(obj, elObj);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
});
|
||||
|
||||
return func(obj);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user