test: 提取公共测试流程

This commit is contained in:
Fu Diwei 2021-06-04 21:44:42 +08:00
parent 3e491dc2f5
commit 0805414a42
15 changed files with 718 additions and 960 deletions

View File

@ -16,15 +16,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV3", "src\SKIT.FlurlHttpClient.Wechat.TenpayV3\SKIT.FlurlHttpClient.Wechat.TenpayV3.csproj", "{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.Work", "src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj", "{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Work", "src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj", "{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C95AF531-CF44-44AA-AC90-F4DF9F941674}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{4C48D9D5-1D7F-4616-A05D-256555C310FC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TestTools", "test\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj", "{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Api.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Api.UnitTests\SKIT.FlurlHttpClient.Wechat.Api.UnitTests.csproj", "{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests.csproj", "{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.Work.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj", "{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Work.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj", "{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -44,6 +48,14 @@ Global
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Release|Any CPU.Build.0 = Release|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.Build.0 = Release|Any CPU
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Release|Any CPU.Build.0 = Release|Any CPU
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -52,10 +64,6 @@ Global
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Release|Any CPU.Build.0 = Release|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.Build.0 = Release|Any CPU
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -68,10 +76,12 @@ Global
{63F7116F-320A-4CD8-9B84-2C675412F70F} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{082C1F69-7932-473F-A700-49584371BE8C} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3} = {4C48D9D5-1D7F-4616-A05D-256555C310FC}
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{4C48D9D5-1D7F-4616-A05D-256555C310FC} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F08ED64E-2517-4B51-A4BE-D33D56CC7B39}

View File

@ -33,6 +33,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.Api\SKIT.FlurlHttpClient.Wechat.Api.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests
{
public class WechatApiDeclarationTests
{
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Api");
[Fact(DisplayName = "验证 API 模型命名")]
public void ApiModelsNamingTest()
{
TestAssertUtil.VerifyApiModelsNaming(_assembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "验证 API 模型定义")]
public void ApiModelsDefinitionTest()
{
string workdir = Path.Combine(Environment.CurrentDirectory, "ModelSamples");
Assert.True(Directory.Exists(workdir));
TestAssertUtil.VerifyApiModelsDefinition(_assembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "验证 API 事件定义")]
public void ApiEventsDefinitionTest()
{
string workdir = Path.Combine(Environment.CurrentDirectory, "EventSamples");
Assert.True(Directory.Exists(workdir));
TestAssertUtil.VerifyApiEventsDefinition(_assembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "验证 API 接口命名")]
public void ApiExtensionsNamingTest()
{
TestAssertUtil.VerifyApiExtensionsNaming(_assembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -1,347 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests
{
public class WechatApiDefinitionTests
{
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Api");
[Fact(DisplayName = "验证模型定义")]
public void ModelDefinitionsTest()
{
static void TrySetPropertiesValueRecursively(object obj)
{
var lstProperty = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var tProperty in lstProperty)
{
if (tProperty.SetMethod == null || !tProperty.SetMethod.IsPublic)
continue;
var newtonsoftJsonAttribute = tProperty.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>();
var systemTextJsonAttribute = tProperty.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
throw new Exception($"`{obj.GetType().Name}` fields mismatching: `{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`");
if (tProperty.PropertyType.IsPrimitive)
{
// noop
}
else if (tProperty.PropertyType.IsArray)
{
Type tEl = tProperty.PropertyType.Assembly.GetType(tProperty.PropertyType.FullName.Replace("[]", string.Empty));
object propEl = (tEl == typeof(string)) ? string.Empty : Activator.CreateInstance(tEl);
propEl = Convert.ChangeType(propEl, tEl);
TrySetPropertiesValueRecursively(propEl);
Array prop = Array.CreateInstance(tEl, 1);
prop.SetValue(propEl, 0);
tProperty.SetValue(obj, prop);
}
else if (tProperty.PropertyType == typeof(string))
{
tProperty.SetValue(obj, string.Empty);
}
else if (tProperty.PropertyType.Namespace == "System" &&
tProperty.PropertyType.Name.StartsWith("Nullable"))
{
// noop
}
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
(tProperty.PropertyType.Name.StartsWith("IDictionary") || tProperty.PropertyType.Name.StartsWith("Dictionary")))
{
// noop
}
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
(tProperty.PropertyType.Name.StartsWith("IList") || tProperty.PropertyType.Name.StartsWith("List")))
{
Type tGeneric = tProperty.PropertyType.GetGenericArguments().Single();
object propEl = (tGeneric == typeof(string)) ? string.Empty : Activator.CreateInstance(tGeneric);
propEl = Convert.ChangeType(propEl, tGeneric);
TrySetPropertiesValueRecursively(propEl);
Type tList = typeof(List<>).MakeGenericType(new Type[] { tGeneric });
object prop = Activator.CreateInstance(tList);
tList.GetMethod("Add").Invoke(prop, new[] { propEl });
tProperty.SetValue(obj, prop);
}
else
{
object prop = Activator.CreateInstance(tProperty.PropertyType);
TrySetPropertiesValueRecursively(prop);
tProperty.SetValue(obj, prop);
}
}
};
var lstModel = _assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(_assembly.GetName().Name + ".Models") &&
e.IsClass &&
!e.IsAbstract &&
!e.IsInterface &&
!e.IsNested
)
.ToList();
var exceptions = new List<Type>();
foreach (Type tModel in lstModel)
{
// 模型命名结尾必为 Request 或 Response
if (!tModel.Name.EndsWith("Request") && !tModel.Name.EndsWith("Response"))
{
exceptions.Add(tModel);
continue;
}
// Request 必继承自 WechatApiRequest、且有同名 Response
if (tModel.Name.EndsWith("Request"))
{
if (!typeof(WechatApiRequest).IsAssignableFrom(tModel))
{
exceptions.Add(tModel);
continue;
}
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Request".Length)}Response"))
{
exceptions.Add(tModel);
continue;
}
}
// Request 必继承自 WechatApiResponse、且有同名 Request
if (tModel.Name.EndsWith("Response"))
{
if (!typeof(WechatApiResponse).IsAssignableFrom(tModel))
{
exceptions.Add(tModel);
continue;
}
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Response".Length)}Request"))
{
exceptions.Add(tModel);
continue;
}
}
// 递归构造模型,并尝试 JSON 序列化以检测是否有序列化问题
try
{
object instance = _assembly.CreateInstance(tModel.Namespace + "." + tModel.Name);
TrySetPropertiesValueRecursively(instance);
new FlurlNewtonsoftJsonSerializer().Serialize(instance);
new FlurlSystemTextJsonSerializer().Serialize(instance);
}
catch (Exception ex)
{
throw new Exception($"Serialize `{tModel.Name}` failed.", ex);
}
}
Assert.Empty(exceptions);
}
[Fact(DisplayName = "验证接口定义")]
public void InterfaceDefinitionsTest()
{
var lstInterface = _assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(_assembly.GetName().Name) &&
e.Name.StartsWith("WechatApiClientExecute") &&
e.Name.EndsWith("Extensions")
)
.ToList();
var exceptions = new List<MethodInfo>();
foreach (Type tInterface in lstInterface)
{
var lstMethod = tInterface.GetMethods()
.Where(e =>
e.IsPublic &&
e.IsStatic &&
e.GetParameters().FirstOrDefault().ParameterType == typeof(WechatApiClient)
)
.ToList();
foreach (MethodInfo tMethod in lstMethod)
{
var lstParam = tMethod.GetParameters();
// 参数签名必为 this client + request + cancelToken
if (lstParam.Length != 3)
{
exceptions.Add(tMethod);
continue;
}
// 第二个参数必为 WechatApiRequest 子类
if (!typeof(WechatApiRequest).IsAssignableFrom(lstParam[1].ParameterType))
{
exceptions.Add(tMethod);
continue;
}
// 方法名与第二个参数、返回值均有相同命名
string func = tMethod.Name;
string para = lstParam[1].ParameterType.Name;
string retv = tMethod.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
if (para == null || !para.EndsWith("Request"))
{
exceptions.Add(tMethod);
continue;
}
else if (retv == null || !retv.EndsWith("Response"))
{
if (!tMethod.ReturnType.GenericTypeArguments.First().IsGenericType)
{
exceptions.Add(tMethod);
}
continue;
}
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
{
exceptions.Add(tMethod);
continue;
}
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
{
exceptions.Add(tMethod);
continue;
}
}
}
Assert.Empty(exceptions);
}
[Fact(DisplayName = "验证字段定义")]
public void FieldDefinitionsTest()
{
var exceptions = new List<string>();
string[] GetAllFiles(string path)
{
var 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();
}
void VerifyJsonSamples(string subdir, string subns)
{
string workdir = Path.Combine(Environment.CurrentDirectory, subdir);
Assert.True(Directory.Exists(workdir));
var lstFile = GetAllFiles(workdir)
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
.ToList();
Assert.NotEmpty(lstFile);
foreach (string file in lstFile)
{
string json = File.ReadAllText(file);
string name = Path.GetFileNameWithoutExtension(file);
Type type = _assembly.GetType($"{_assembly.GetName().Name}.{subns}.{name}");
if (type == null)
{
exceptions.Add(name);
continue;
}
try
{
var settings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
settings.CheckAdditionalContent = true;
settings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
new FlurlNewtonsoftJsonSerializer(settings).Deserialize(json, type);
new FlurlSystemTextJsonSerializer().Deserialize(json, type);
}
catch (Exception ex)
{
exceptions.Add(name);
if (ex is Newtonsoft.Json.JsonException)
throw new Exception($"Deserialize `{name}` by Newtonsoft.Json failed.", ex);
else if (ex is System.Text.Json.JsonException)
throw new Exception($"Deserialize `{name}` by System.Text.Json failed.", ex);
else
throw new Exception($"Deserialize `{name}` failed.", ex);
}
}
}
void VerifyXmlSamples(string subdir, string subns)
{
string workdir = Path.Combine(Environment.CurrentDirectory, subdir);
Assert.True(Directory.Exists(workdir));
var lstFile = GetAllFiles(workdir)
.Where(e => string.Equals(Path.GetExtension(e), ".xml", StringComparison.InvariantCultureIgnoreCase))
.ToList();
Assert.NotEmpty(lstFile);
foreach (string file in lstFile)
{
string xml = File.ReadAllText(file);
string name = Path.GetFileNameWithoutExtension(file);
Type type = _assembly.GetType($"{_assembly.GetName().Name}.{subns}.{name}");
if (type == null)
{
exceptions.Add(name);
continue;
}
try
{
using StringReader reader = new StringReader(xml);
System.Xml.Serialization.XmlSerializer xmlSerializer = new System.Xml.Serialization.XmlSerializer(type, new System.Xml.Serialization.XmlRootAttribute("xml"));
xmlSerializer.Deserialize(reader);
}
catch (Exception ex)
{
exceptions.Add(name);
if (ex is System.Xml.XmlException)
throw new Exception($"Deserialize `{name}` by System.Xml failed.", ex);
else
throw new Exception($"Deserialize `{name}` failed.", ex);
}
}
}
VerifyJsonSamples("ModelSamples", "Models");
VerifyJsonSamples("EventSamples", "Events");
VerifyXmlSamples("EventSamples", "Events");
Assert.Empty(exceptions);
}
}
}

View File

@ -30,6 +30,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.TenpayV3\SKIT.FlurlHttpClient.Wechat.TenpayV3.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -7,7 +7,7 @@ using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
{
public class WechatTenpayConverterTests
public class WechatConverterTests
{
class JsonDateTimeOffsetTestEntity
{

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
{
public class WechatTenpayDeclarationTests
{
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.TenpayV3");
[Fact(DisplayName = "验证 API 模型命名")]
public void ApiModelsNamingTest()
{
TestAssertUtil.VerifyApiModelsNaming(_assembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "验证 API 模型定义")]
public void ApiModelsDefinitionTest()
{
string workdir = Path.Combine(Environment.CurrentDirectory, "ModelSamples");
Assert.True(Directory.Exists(workdir));
TestAssertUtil.VerifyApiModelsDefinition(_assembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "验证 API 事件定义")]
public void ApiEventsDefinitionTest()
{
string workdir = Path.Combine(Environment.CurrentDirectory, "EventSamples");
Assert.True(Directory.Exists(workdir));
TestAssertUtil.VerifyApiEventsDefinition(_assembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "验证 API 接口命名")]
public void ApiExtensionsNamingTest()
{
TestAssertUtil.VerifyApiExtensionsNaming(_assembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -1,303 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
{
public class WechatTenpayDefinitionTests
{
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.TenpayV3");
[Fact(DisplayName = "验证模型定义")]
public void ModelDefinitionsTest()
{
static void TrySetPropertiesValueRecursively(object obj)
{
var lstProperty = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var tProperty in lstProperty)
{
if (tProperty.SetMethod == null || !tProperty.SetMethod.IsPublic)
continue;
var newtonsoftJsonAttribute = tProperty.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>();
var systemTextJsonAttribute = tProperty.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
throw new Exception($"`{obj.GetType().Name}` fields mismatching: `{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`");
if (tProperty.PropertyType.IsPrimitive)
{
// noop
}
else if (tProperty.PropertyType.IsArray)
{
Type tEl = tProperty.PropertyType.Assembly.GetType(tProperty.PropertyType.FullName.Replace("[]", string.Empty));
object propEl = (tEl == typeof(string)) ? string.Empty : Activator.CreateInstance(tEl);
propEl = Convert.ChangeType(propEl, tEl);
TrySetPropertiesValueRecursively(propEl);
Array prop = Array.CreateInstance(tEl, 1);
prop.SetValue(propEl, 0);
tProperty.SetValue(obj, prop);
}
else if (tProperty.PropertyType == typeof(string))
{
tProperty.SetValue(obj, string.Empty);
}
else if (tProperty.PropertyType.Namespace == "System" &&
tProperty.PropertyType.Name.StartsWith("Nullable"))
{
// noop
}
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
(tProperty.PropertyType.Name.StartsWith("IDictionary") || tProperty.PropertyType.Name.StartsWith("Dictionary")))
{
// noop
}
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
(tProperty.PropertyType.Name.StartsWith("IList") || tProperty.PropertyType.Name.StartsWith("List")))
{
Type tGeneric = tProperty.PropertyType.GetGenericArguments().Single();
object propEl = (tGeneric == typeof(string)) ? string.Empty : Activator.CreateInstance(tGeneric);
propEl = Convert.ChangeType(propEl, tGeneric);
TrySetPropertiesValueRecursively(propEl);
Type tList = typeof(List<>).MakeGenericType(new Type[] { tGeneric });
object prop = Activator.CreateInstance(tList);
tList.GetMethod("Add").Invoke(prop, new[] { propEl });
tProperty.SetValue(obj, prop);
}
else
{
object prop = Activator.CreateInstance(tProperty.PropertyType);
TrySetPropertiesValueRecursively(prop);
tProperty.SetValue(obj, prop);
}
}
};
var lstModel = _assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(_assembly.GetName().Name + ".Models") &&
e.IsClass &&
!e.IsAbstract &&
!e.IsInterface &&
!e.IsNested
)
.ToList();
var exceptions = new List<Type>();
foreach (Type tModel in lstModel)
{
// 模型命名结尾必为 Request 或 Response
if (!tModel.Name.EndsWith("Request") && !tModel.Name.EndsWith("Response"))
{
exceptions.Add(tModel);
continue;
}
// Request 必继承自 WechatTenpayRequest、且有同名 Response
if (tModel.Name.EndsWith("Request"))
{
if (!typeof(WechatTenpayRequest).IsAssignableFrom(tModel))
{
exceptions.Add(tModel);
continue;
}
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Request".Length)}Response"))
{
exceptions.Add(tModel);
continue;
}
}
// Request 必继承自 WechatTenpayResponse、且有同名 Request
if (tModel.Name.EndsWith("Response"))
{
if (!typeof(WechatTenpayResponse).IsAssignableFrom(tModel))
{
exceptions.Add(tModel);
continue;
}
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Response".Length)}Request"))
{
exceptions.Add(tModel);
continue;
}
}
// 递归构造模型,并尝试 JSON 序列化以检测是否有序列化问题
try
{
object instance = _assembly.CreateInstance(tModel.Namespace + "." + tModel.Name);
TrySetPropertiesValueRecursively(instance);
new FlurlNewtonsoftJsonSerializer().Serialize(instance);
new FlurlSystemTextJsonSerializer().Serialize(instance);
}
catch (Exception ex)
{
throw new Exception($"Serialize `{tModel.Name}` failed.", ex);
}
}
Assert.Empty(exceptions);
}
[Fact(DisplayName = "验证接口定义")]
public void InterfaceDefinitionsTest()
{
var lstInterface = _assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(_assembly.GetName().Name) &&
e.Name.StartsWith("WechatTenpayClientExecute") &&
e.Name.EndsWith("Extensions")
)
.ToList();
var exceptions = new List<MethodInfo>();
foreach (Type tInterface in lstInterface)
{
var lstMethod = tInterface.GetMethods()
.Where(e =>
e.IsPublic &&
e.IsStatic &&
e.GetParameters().FirstOrDefault().ParameterType == typeof(WechatTenpayClient)
)
.ToList();
foreach (MethodInfo tMethod in lstMethod)
{
var lstParam = tMethod.GetParameters();
// 参数签名必为 this client + request + cancelToken
if (lstParam.Length != 3)
{
exceptions.Add(tMethod);
continue;
}
// 第二个参数必为 WechatTenpayRequest 子类
if (!typeof(WechatTenpayRequest).IsAssignableFrom(lstParam[1].ParameterType))
{
exceptions.Add(tMethod);
continue;
}
// 方法名与第二个参数、返回值均有相同命名
string func = tMethod.Name;
string para = lstParam[1].ParameterType.Name;
string retv = tMethod.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
if (para == null || !para.EndsWith("Request"))
{
exceptions.Add(tMethod);
continue;
}
else if (retv == null || !retv.EndsWith("Response"))
{
exceptions.Add(tMethod);
continue;
}
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
{
exceptions.Add(tMethod);
continue;
}
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
{
exceptions.Add(tMethod);
continue;
}
}
}
Assert.Empty(exceptions);
}
[Fact(DisplayName = "验证字段定义")]
public void FieldDefinitionsTest()
{
var exceptions = new List<string>();
void VerifyJsonSamples(string subdir, string subns)
{
string[] GetFiles(string path)
{
var results = new List<string>();
string[] dirs = Directory.GetDirectories(path);
string[] files = Directory.GetFiles(path);
results.AddRange(files);
foreach (string dir in dirs)
{
results.AddRange(GetFiles(dir));
}
return results.ToArray();
}
string workdir = Path.Combine(Environment.CurrentDirectory, subdir);
Assert.True(Directory.Exists(workdir));
var lstFile = GetFiles(workdir)
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
.ToList();
Assert.NotEmpty(lstFile);
foreach (string file in lstFile)
{
string json = File.ReadAllText(file);
string name = Path.GetFileNameWithoutExtension(file);
Type type = _assembly.GetType($"{_assembly.GetName().Name}.{subns}.{name}");
if (type == null)
{
exceptions.Add(name);
continue;
}
try
{
var settings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
settings.CheckAdditionalContent = true;
settings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
new FlurlNewtonsoftJsonSerializer(settings).Deserialize(json, type);
new FlurlSystemTextJsonSerializer().Deserialize(json, type);
}
catch (Exception ex)
{
exceptions.Add(name);
if (ex is Newtonsoft.Json.JsonException)
throw new Exception($"Deserialize `{name}` by Newtonsoft.Json failed.", ex);
else if (ex is System.Text.Json.JsonException)
throw new Exception($"Deserialize `{name}` by System.Text.Json failed.", ex);
else
throw new Exception($"Deserialize `{name}` failed.", ex);
}
}
}
VerifyJsonSamples("ModelSamples", "Models");
VerifyJsonSamples("EventSamples", "Events");
Assert.Empty(exceptions);
}
}
}

View File

@ -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>

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

View 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();
}
}
}

View 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);
}
}
}

View File

@ -27,6 +27,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
{
public class WechatWorkDeclarationTests
{
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Work");
[Fact(DisplayName = "验证 API 模型命名")]
public void ApiModelsNamingTest()
{
TestAssertUtil.VerifyApiModelsNaming(_assembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "验证 API 模型定义")]
public void ApiModelsDefinitionTest()
{
string workdir = Path.Combine(Environment.CurrentDirectory, "ModelSamples");
Assert.True(Directory.Exists(workdir));
TestAssertUtil.VerifyApiModelsDefinition(_assembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "验证 API 接口命名")]
public void ApiExtensionsNamingTest()
{
TestAssertUtil.VerifyApiExtensionsNaming(_assembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -1,302 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
{
public class WechatWorkDefinitionTests
{
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Work");
[Fact(DisplayName = "验证模型定义")]
public void ModelDefinitionsTest()
{
static void TrySetPropertiesValueRecursively(object obj)
{
var lstProperty = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var tProperty in lstProperty)
{
if (tProperty.SetMethod == null || !tProperty.SetMethod.IsPublic)
continue;
var newtonsoftJsonAttribute = tProperty.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>();
var systemTextJsonAttribute = tProperty.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
throw new Exception($"`{obj.GetType().Name}` fields mismatching: `{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`");
if (tProperty.PropertyType.IsPrimitive)
{
// noop
}
else if (tProperty.PropertyType.IsArray)
{
Type tEl = tProperty.PropertyType.Assembly.GetType(tProperty.PropertyType.FullName.Replace("[]", string.Empty));
object propEl = (tEl == typeof(string)) ? string.Empty : Activator.CreateInstance(tEl);
propEl = Convert.ChangeType(propEl, tEl);
TrySetPropertiesValueRecursively(propEl);
Array prop = Array.CreateInstance(tEl, 1);
prop.SetValue(propEl, 0);
tProperty.SetValue(obj, prop);
}
else if (tProperty.PropertyType == typeof(string))
{
tProperty.SetValue(obj, string.Empty);
}
else if (tProperty.PropertyType.Namespace == "System" &&
tProperty.PropertyType.Name.StartsWith("Nullable"))
{
// noop
}
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
(tProperty.PropertyType.Name.StartsWith("IDictionary") || tProperty.PropertyType.Name.StartsWith("Dictionary")))
{
// noop
}
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
(tProperty.PropertyType.Name.StartsWith("IList") || tProperty.PropertyType.Name.StartsWith("List")))
{
Type tGeneric = tProperty.PropertyType.GetGenericArguments().Single();
object propEl = (tGeneric == typeof(string)) ? string.Empty : Activator.CreateInstance(tGeneric);
propEl = Convert.ChangeType(propEl, tGeneric);
TrySetPropertiesValueRecursively(propEl);
Type tList = typeof(List<>).MakeGenericType(new Type[] { tGeneric });
object prop = Activator.CreateInstance(tList);
tList.GetMethod("Add").Invoke(prop, new[] { propEl });
tProperty.SetValue(obj, prop);
}
else
{
object prop = Activator.CreateInstance(tProperty.PropertyType);
TrySetPropertiesValueRecursively(prop);
tProperty.SetValue(obj, prop);
}
}
};
var lstModel = _assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(_assembly.GetName().Name + ".Models") &&
e.IsClass &&
!e.IsAbstract &&
!e.IsInterface &&
!e.IsNested
)
.ToList();
var exceptions = new List<Type>();
foreach (Type tModel in lstModel)
{
// 模型命名结尾必为 Request 或 Response
if (!tModel.Name.EndsWith("Request") && !tModel.Name.EndsWith("Response"))
{
exceptions.Add(tModel);
continue;
}
// Request 必继承自 WechatTenpayRequest、且有同名 Response
if (tModel.Name.EndsWith("Request"))
{
if (!typeof(WechatWorkRequest).IsAssignableFrom(tModel))
{
exceptions.Add(tModel);
continue;
}
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Request".Length)}Response"))
{
exceptions.Add(tModel);
continue;
}
}
// Request 必继承自 WechatTenpayResponse、且有同名 Request
if (tModel.Name.EndsWith("Response"))
{
if (!typeof(WechatWorkResponse).IsAssignableFrom(tModel))
{
exceptions.Add(tModel);
continue;
}
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Response".Length)}Request"))
{
exceptions.Add(tModel);
continue;
}
}
// 递归构造模型,并尝试 JSON 序列化以检测是否有序列化问题
try
{
object instance = _assembly.CreateInstance(tModel.Namespace + "." + tModel.Name);
TrySetPropertiesValueRecursively(instance);
new FlurlNewtonsoftJsonSerializer().Serialize(instance);
new FlurlSystemTextJsonSerializer().Serialize(instance);
}
catch (Exception ex)
{
throw new Exception($"Serialize `{tModel.Name}` failed.", ex);
}
}
Assert.Empty(exceptions);
}
[Fact(DisplayName = "验证接口定义")]
public void InterfaceDefinitionsTest()
{
var lstInterface = _assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(_assembly.GetName().Name) &&
e.Name.StartsWith("WechatTenpayClientExecute") &&
e.Name.EndsWith("Extensions")
)
.ToList();
var exceptions = new List<MethodInfo>();
foreach (Type tInterface in lstInterface)
{
var lstMethod = tInterface.GetMethods()
.Where(e =>
e.IsPublic &&
e.IsStatic &&
e.GetParameters().FirstOrDefault().ParameterType == typeof(WechatWorkClient)
)
.ToList();
foreach (MethodInfo tMethod in lstMethod)
{
var lstParam = tMethod.GetParameters();
// 参数签名必为 this client + request + cancelToken
if (lstParam.Length != 3)
{
exceptions.Add(tMethod);
continue;
}
// 第二个参数必为 WechatTenpayRequest 子类
if (!typeof(WechatWorkRequest).IsAssignableFrom(lstParam[1].ParameterType))
{
exceptions.Add(tMethod);
continue;
}
// 方法名与第二个参数、返回值均有相同命名
string func = tMethod.Name;
string para = lstParam[1].ParameterType.Name;
string retv = tMethod.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
if (para == null || !para.EndsWith("Request"))
{
exceptions.Add(tMethod);
continue;
}
else if (retv == null || !retv.EndsWith("Response"))
{
exceptions.Add(tMethod);
continue;
}
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
{
exceptions.Add(tMethod);
continue;
}
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
{
exceptions.Add(tMethod);
continue;
}
}
}
Assert.Empty(exceptions);
}
[Fact(DisplayName = "验证字段定义")]
public void FieldDefinitionsTest()
{
var exceptions = new List<string>();
string[] GetAllFiles(string path)
{
var 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();
}
void VerifyJsonSamples(string subdir, string subns)
{
string workdir = Path.Combine(Environment.CurrentDirectory, subdir);
Assert.True(Directory.Exists(workdir));
var lstFile = GetAllFiles(workdir)
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
.ToList();
Assert.NotEmpty(lstFile);
foreach (string file in lstFile)
{
string json = File.ReadAllText(file);
string name = Path.GetFileNameWithoutExtension(file);
Type type = _assembly.GetType($"{_assembly.GetName().Name}.{subns}.{name}");
if (type == null)
{
exceptions.Add(name);
continue;
}
try
{
var settings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
settings.CheckAdditionalContent = true;
settings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
new FlurlNewtonsoftJsonSerializer(settings).Deserialize(json, type);
new FlurlSystemTextJsonSerializer().Deserialize(json, type);
}
catch (Exception ex)
{
exceptions.Add(name);
if (ex is Newtonsoft.Json.JsonException)
throw new Exception($"Deserialize `{name}` by Newtonsoft.Json failed.", ex);
else if (ex is System.Text.Json.JsonException)
throw new Exception($"Deserialize `{name}` by System.Text.Json failed.", ex);
else
throw new Exception($"Deserialize `{name}` failed.", ex);
}
}
}
VerifyJsonSamples("ModelSamples", "Models");
Assert.Empty(exceptions);
}
}
}