183 lines
7.1 KiB
C#
183 lines
7.1 KiB
C#
using System.Collections.Generic;
|
||
using FluentAssertions;
|
||
using Scripting.Application.DTOs;
|
||
using Scripting.Application.Services;
|
||
using Xunit;
|
||
|
||
namespace BuildFlow.Scripting.Tests;
|
||
|
||
/// <summary>
|
||
/// <see cref="ScriptTypeRegistry"/> 单元测试:反射扫描、关键字/Kind 过滤、分页与类型格式化。
|
||
/// 扫描依赖测试运行时加载的 BuildFlow.* 程序集;本测试项目通过 Scripting.Application
|
||
/// 传递引用了 Platform.Application,其 Query 类型应被扫描到。
|
||
/// </summary>
|
||
public class ScriptTypeRegistryTests
|
||
{
|
||
private readonly ScriptTypeRegistry _registry = new();
|
||
|
||
public ScriptTypeRegistryTests()
|
||
{
|
||
// 测试构造时扫描一次,保证后续断言基于已填充状态
|
||
_registry.Scan();
|
||
}
|
||
|
||
/// <summary>扫描应至少发现一个 BuildFlow.* IBaseRequest 实现(来自 Platform.Application)。</summary>
|
||
[Fact]
|
||
public void Scan_Populates_Known_MediatR_Types()
|
||
{
|
||
var all = _registry.GetAll();
|
||
all.Should().NotBeEmpty("Platform.Application 含大量 IRequest 实现,扫描后应非空");
|
||
|
||
// 抽样:应能找到带 Query/Command 后缀的类型(启发式分类的输入)
|
||
all.Should().Contain(m => m.Kind == "Query" || m.Kind == "Command");
|
||
}
|
||
|
||
/// <summary>关键字搜索应按 名/全名/命名空间 模糊过滤。</summary>
|
||
[Fact]
|
||
public void Search_By_Keyword_Filters()
|
||
{
|
||
var all = _registry.GetAll();
|
||
all.Should().NotBeEmpty();
|
||
|
||
// 任取一个类型的名字片段作为关键字,搜索结果必须全部命中该关键字
|
||
var keyword = PickProbe(all);
|
||
var results = _registry.Search(keyword, kind: null, pageIndex: 1, pageSize: 100).ToList();
|
||
|
||
results.Should().NotBeEmpty($"关键字 '{keyword}' 来源于已扫描类型");
|
||
foreach (var m in results)
|
||
{
|
||
var hit = m.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase)
|
||
|| m.FullName.Contains(keyword, StringComparison.OrdinalIgnoreCase)
|
||
|| m.Namespace.Contains(keyword, StringComparison.OrdinalIgnoreCase);
|
||
hit.Should().BeTrue($"结果 '{m.FullName}' 应匹配关键字 '{keyword}'");
|
||
}
|
||
}
|
||
|
||
/// <summary>Kind=Query 过滤后,结果应全部是 Query;Total 计数应与分页前一致。</summary>
|
||
[Fact]
|
||
public void Search_By_Kind_Query_Filters()
|
||
{
|
||
if (_registry.GetAll().Count == 0)
|
||
{
|
||
// 没有类型可过滤时跳过(不应失败)
|
||
return;
|
||
}
|
||
|
||
var queryTotal = _registry.Total(keyword: null, kind: "Query");
|
||
var queryPage = _registry.Search(keyword: null, kind: "Query", pageIndex: 1, pageSize: 1000).ToList();
|
||
|
||
queryPage.Count.Should().Be(queryTotal, "Total 与 Search(不限页大小)计数应一致");
|
||
queryPage.Should().AllSatisfy(m => m.Kind.Should().Be("Query"));
|
||
queryTotal.Should().BeGreaterThan(0, "Platform.Application 至少含一个 Query");
|
||
}
|
||
|
||
/// <summary>分页:第二页应正确跳过第一页条数。</summary>
|
||
[Fact]
|
||
public void Search_Paginates()
|
||
{
|
||
var total = _registry.Total(keyword: null, kind: null);
|
||
if (total < 2) return;
|
||
|
||
var firstPageFirst = _registry.Search(null, null, pageIndex: 1, pageSize: 1).First();
|
||
var secondPageFirst = _registry.Search(null, null, pageIndex: 2, pageSize: 1).First();
|
||
firstPageFirst.FullName.Should().NotBe(secondPageFirst.FullName, "翻页后首页首项应不同于第二页首项");
|
||
}
|
||
|
||
/// <summary>ResponseType 字段:IRequest<T> 实现应能解析出 T 的友好名。</summary>
|
||
[Fact]
|
||
public void BuildMeta_Extracts_Response_Type()
|
||
{
|
||
var all = _registry.GetAll();
|
||
all.Should().Contain(m => m.ResponseType != null,
|
||
"至少一个类型实现了 IRequest<T>,应解析出非空 ResponseType");
|
||
}
|
||
|
||
/// <summary>重复扫描是幂等的:再次扫描后总数不变。</summary>
|
||
[Fact]
|
||
public void Scan_Is_Idempotent()
|
||
{
|
||
var before = _registry.GetAll().Count;
|
||
_registry.Scan();
|
||
var after = _registry.GetAll().Count;
|
||
after.Should().Be(before, "重复扫描不应改变类型集合");
|
||
}
|
||
|
||
// -------- FormatTypeName(internal static,经 InternalsVisibleTo 暴露) --------
|
||
|
||
[Theory]
|
||
[InlineData(typeof(int), "int")]
|
||
[InlineData(typeof(string), "string")]
|
||
[InlineData(typeof(bool), "bool")]
|
||
[InlineData(typeof(long), "long")]
|
||
[InlineData(typeof(decimal), "decimal")]
|
||
[InlineData(typeof(double), "double")]
|
||
[InlineData(typeof(Guid), "Guid")]
|
||
[InlineData(typeof(DateTime), "DateTime")]
|
||
[InlineData(typeof(object), "object")]
|
||
public void FormatTypeName_Primitives_Use_CSharp_Alias(Type type, string expected)
|
||
{
|
||
ScriptTypeRegistry.FormatTypeName(type).Should().Be(expected);
|
||
}
|
||
|
||
[Fact]
|
||
public void FormatTypeName_Null_Returns_Question_Mark()
|
||
{
|
||
ScriptTypeRegistry.FormatTypeName(null).Should().Be("?");
|
||
}
|
||
|
||
[Fact]
|
||
public void FormatTypeName_Nullable_Primitive_Appends_Question_Mark()
|
||
{
|
||
ScriptTypeRegistry.FormatTypeName(typeof(int?)).Should().Be("int?");
|
||
ScriptTypeRegistry.FormatTypeName(typeof(Guid?)).Should().Be("Guid?");
|
||
ScriptTypeRegistry.FormatTypeName(typeof(DateTime?)).Should().Be("DateTime?");
|
||
}
|
||
|
||
[Fact]
|
||
public void FormatTypeName_Generic_Collection_Formats_With_Angle_Brackets()
|
||
{
|
||
ScriptTypeRegistry.FormatTypeName(typeof(List<int>)).Should().Be("List<int>");
|
||
ScriptTypeRegistry.FormatTypeName(typeof(IEnumerable<string>)).Should().Be("IEnumerable<string>");
|
||
ScriptTypeRegistry.FormatTypeName(typeof(IReadOnlyList<Guid>)).Should().Be("IReadOnlyList<Guid>");
|
||
}
|
||
|
||
[Fact]
|
||
public void FormatTypeName_Dictionary_Formats_Two_Type_Arguments()
|
||
{
|
||
ScriptTypeRegistry.FormatTypeName(typeof(Dictionary<string, int>))
|
||
.Should().Be("Dictionary<string, int>");
|
||
}
|
||
|
||
[Fact]
|
||
public void FormatTypeName_Nested_Generic_Formats_Recursively()
|
||
{
|
||
ScriptTypeRegistry.FormatTypeName(typeof(List<List<string>>))
|
||
.Should().Be("List<List<string>>");
|
||
}
|
||
|
||
[Fact]
|
||
public void FormatTypeName_Array_Appends_Brackets()
|
||
{
|
||
ScriptTypeRegistry.FormatTypeName(typeof(int[])).Should().Be("int[]");
|
||
ScriptTypeRegistry.FormatTypeName(typeof(string[])).Should().Be("string[]");
|
||
ScriptTypeRegistry.FormatTypeName(typeof(List<int>[])).Should().Be("List<int>[]");
|
||
}
|
||
|
||
[Fact]
|
||
public void FormatTypeName_Complex_Type_Uses_FullName()
|
||
{
|
||
ScriptTypeRegistry.FormatTypeName(typeof(System.Text.Json.JsonElement))
|
||
.Should().Be("System.Text.Json.JsonElement");
|
||
}
|
||
|
||
// -------- 辅助 --------
|
||
|
||
/// <summary>从已扫描集合中挑一个适合做关键字探测的样本(取名字的子串,避免取到单字符)。</summary>
|
||
private static string PickProbe(IReadOnlyCollection<ScriptTypeMeta> all)
|
||
{
|
||
// 选一个名字至少 4 字符的类型,截取前 4 字符作为关键字,保证可复现命中
|
||
var pick = all.First(m => m.Name.Length >= 4);
|
||
return pick.Name[..4];
|
||
}
|
||
}
|