using System.Collections.Generic; using FluentAssertions; using Scripting.Application.DTOs; using Scripting.Application.Services; using Xunit; namespace BuildFlow.Scripting.Tests; /// /// 单元测试:反射扫描、关键字/Kind 过滤、分页与类型格式化。 /// 扫描依赖测试运行时加载的 BuildFlow.* 程序集;本测试项目通过 Scripting.Application /// 传递引用了 Platform.Application,其 Query 类型应被扫描到。 /// public class ScriptTypeRegistryTests { private readonly ScriptTypeRegistry _registry = new(); public ScriptTypeRegistryTests() { // 测试构造时扫描一次,保证后续断言基于已填充状态 _registry.Scan(); } /// 扫描应至少发现一个 BuildFlow.* IBaseRequest 实现(来自 Platform.Application)。 [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"); } /// 关键字搜索应按 名/全名/命名空间 模糊过滤。 [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}'"); } } /// Kind=Query 过滤后,结果应全部是 Query;Total 计数应与分页前一致。 [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"); } /// 分页:第二页应正确跳过第一页条数。 [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, "翻页后首页首项应不同于第二页首项"); } /// ResponseType 字段:IRequest<T> 实现应能解析出 T 的友好名。 [Fact] public void BuildMeta_Extracts_Response_Type() { var all = _registry.GetAll(); all.Should().Contain(m => m.ResponseType != null, "至少一个类型实现了 IRequest,应解析出非空 ResponseType"); } /// 重复扫描是幂等的:再次扫描后总数不变。 [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)).Should().Be("List"); ScriptTypeRegistry.FormatTypeName(typeof(IEnumerable)).Should().Be("IEnumerable"); ScriptTypeRegistry.FormatTypeName(typeof(IReadOnlyList)).Should().Be("IReadOnlyList"); } [Fact] public void FormatTypeName_Dictionary_Formats_Two_Type_Arguments() { ScriptTypeRegistry.FormatTypeName(typeof(Dictionary)) .Should().Be("Dictionary"); } [Fact] public void FormatTypeName_Nested_Generic_Formats_Recursively() { ScriptTypeRegistry.FormatTypeName(typeof(List>)) .Should().Be("List>"); } [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[])).Should().Be("List[]"); } [Fact] public void FormatTypeName_Complex_Type_Uses_FullName() { ScriptTypeRegistry.FormatTypeName(typeof(System.Text.Json.JsonElement)) .Should().Be("System.Text.Json.JsonElement"); } // -------- 辅助 -------- /// 从已扫描集合中挑一个适合做关键字探测的样本(取名字的子串,避免取到单字符)。 private static string PickProbe(IReadOnlyCollection all) { // 选一个名字至少 4 字符的类型,截取前 4 字符作为关键字,保证可复现命中 var pick = all.First(m => m.Name.Length >= 4); return pick.Name[..4]; } }