From 5e9b903d0f468acb7f7fc5bafa9785a6afc7b05a Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 12 May 2026 08:39:00 +0800 Subject: [PATCH] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=20registrar=20?= =?UTF-8?q?=E6=BF=80=E6=B4=BB=E5=A4=B1=E8=B4=A5=E5=88=86=E6=94=AF=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 补充 generated registry 为抽象类型时的回退与抛错覆盖 - 补充 generated registry 缺少无参构造器时的回退与抛错覆盖 --- ...qrsHandlerRegistrarFallbackFailureTests.cs | 131 ++++++++++++++++++ .../Cqrs/CqrsHandlerRegistrarTests.cs | 120 ++++++++++++++++ 2 files changed, 251 insertions(+) diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs index ebfa6659..ffbe5196 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs @@ -138,6 +138,68 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests }); } + /// + /// 验证当 generated registry 是抽象类型时,registrar 会记录告警并回退到反射扫描。 + /// + [Test] + public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Is_Abstract() + { + var generatedAssembly = CreateGeneratedRegistryAssembly( + "GFramework.Cqrs.Tests.Cqrs.AbstractGeneratedRegistryAssembly, Version=1.0.0.0", + typeof(AbstractGeneratedNotificationHandlerRegistry)); + generatedAssembly + .Setup(static assembly => assembly.GetTypes()) + .Returns([typeof(GeneratedRegistryNotificationHandler)]); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + Assert.Multiple(() => + { + Assert.That( + GetGeneratedRegistryNotificationHandlerTypes(container), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + Assert.That( + GetWarningLogs().Any(log => + log.Message.Contains("because it is abstract", StringComparison.Ordinal)), + Is.True); + }); + + generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once); + } + + /// + /// 验证当 generated registry 不暴露可访问无参构造器时,registrar 会记录告警并回退到反射扫描。 + /// + [Test] + public void RegisterHandlers_Should_Fall_Back_To_Reflection_When_Generated_Registry_Has_No_Parameterless_Constructor() + { + var generatedAssembly = CreateGeneratedRegistryAssembly( + "GFramework.Cqrs.Tests.Cqrs.NoParameterlessGeneratedRegistryAssembly, Version=1.0.0.0", + typeof(ConstructorArgumentNotificationHandlerRegistry)); + generatedAssembly + .Setup(static assembly => assembly.GetTypes()) + .Returns([typeof(GeneratedRegistryNotificationHandler)]); + + var container = new MicrosoftDiContainer(); + CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object); + + Assert.Multiple(() => + { + Assert.That( + GetGeneratedRegistryNotificationHandlerTypes(container), + Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); + Assert.That( + GetWarningLogs().Any(log => + log.Message.Contains( + "does not expose an accessible parameterless constructor", + StringComparison.Ordinal)), + Is.True); + }); + + generatedAssembly.Verify(static assembly => assembly.GetTypes(), Times.Once); + } + /// /// 创建一个仅通过 generated registry 注册主 handler、并附带指定 fallback 元数据的程序集替身。 /// @@ -161,6 +223,24 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests return generatedAssembly; } + /// + /// 创建一个只声明 generated registry attribute 的程序集替身,用于验证 registry 激活失败后的回退行为。 + /// + /// 用于日志与缓存键的程序集名。 + /// 要暴露给 registrar 的 generated registry 类型。 + /// 已完成基础接线的程序集 mock。 + private static Mock CreateGeneratedRegistryAssembly(string assemblyName, Type registryType) + { + var generatedAssembly = new Mock(); + generatedAssembly + .SetupGet(static assembly => assembly.FullName) + .Returns(assemblyName); + generatedAssembly + .Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false)) + .Returns([new CqrsHandlerRegistryAttribute(registryType)]); + return generatedAssembly; + } + /// /// 提取容器中针对 generated notification 注册的处理器实现类型。 /// @@ -259,4 +339,55 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests .Where(static log => log.Level == LogLevel.Warning) .ToArray(); } + + /// + /// 模拟 generated registry 被错误声明为抽象类型时的激活失败场景。 + /// + private abstract class AbstractGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry + { + /// + /// 抽象 registry 即便具备注册逻辑,也不应被运行时实例化。 + /// + /// 承载处理器映射的服务集合。 + /// 记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + } + } + + /// + /// 模拟 generated registry 缺少可访问无参构造器时的激活失败场景。 + /// + private sealed class ConstructorArgumentNotificationHandlerRegistry : ICqrsHandlerRegistry + { + /// + /// 初始化一个只能通过额外参数构造的测试 registry。 + /// + /// 用于区分测试场景的占位参数。 + public ConstructorArgumentNotificationHandlerRegistry(string marker) + { + ArgumentNullException.ThrowIfNull(marker); + } + + /// + /// 此实现仅用于满足接口契约;本用例关注的是实例化失败前的回退行为。 + /// + /// 承载处理器映射的服务集合。 + /// 记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + } + } } diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs index 967fc6a8..4fa6b124 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs @@ -6,6 +6,7 @@ using GFramework.Core.Architectures; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Internal; using GFramework.Cqrs.Tests.Logging; namespace GFramework.Cqrs.Tests.Cqrs; @@ -170,6 +171,74 @@ internal sealed class CqrsHandlerRegistrarTests Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)])); } + /// + /// 验证 direct generated-registry 激活入口在 registry 为抽象类型时会抛出异常,并保留契约告警。 + /// + [Test] + public void RegisterGeneratedRegistry_Should_Throw_When_Generated_Registry_Is_Abstract() + { + var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning); + var logger = capturingProvider.CreateLogger(nameof(CqrsHandlerRegistrarTests)); + var container = new MicrosoftDiContainer(); + + var exception = Assert.Throws(() => + CqrsHandlerRegistrar.RegisterGeneratedRegistry( + container, + typeof(AbstractGeneratedNotificationHandlerRegistry), + logger)); + + var warningLogs = capturingProvider.Loggers + .SelectMany(static createdLogger => createdLogger.Logs) + .Where(static log => log.Level == LogLevel.Warning) + .ToArray(); + + Assert.Multiple(() => + { + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Does.Contain(typeof(AbstractGeneratedNotificationHandlerRegistry).FullName)); + Assert.That( + warningLogs.Any(log => + log.Message.Contains("because it is abstract", StringComparison.Ordinal)), + Is.True); + Assert.That(container.GetServicesUnsafe, Is.Empty); + }); + } + + /// + /// 验证 direct generated-registry 激活入口在 registry 缺少无参构造器时会抛出异常,并保留契约告警。 + /// + [Test] + public void RegisterGeneratedRegistry_Should_Throw_When_Generated_Registry_Has_No_Parameterless_Constructor() + { + var capturingProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning); + var logger = capturingProvider.CreateLogger(nameof(CqrsHandlerRegistrarTests)); + var container = new MicrosoftDiContainer(); + + var exception = Assert.Throws(() => + CqrsHandlerRegistrar.RegisterGeneratedRegistry( + container, + typeof(ConstructorArgumentNotificationHandlerRegistry), + logger)); + + var warningLogs = capturingProvider.Loggers + .SelectMany(static createdLogger => createdLogger.Logs) + .Where(static log => log.Level == LogLevel.Warning) + .ToArray(); + + Assert.Multiple(() => + { + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Does.Contain(typeof(ConstructorArgumentNotificationHandlerRegistry).FullName)); + Assert.That( + warningLogs.Any(log => + log.Message.Contains( + "does not expose an accessible parameterless constructor", + StringComparison.Ordinal)), + Is.True); + Assert.That(container.GetServicesUnsafe, Is.Empty); + }); + } + /// /// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。 /// @@ -695,4 +764,55 @@ internal sealed class CqrsHandlerRegistrarTests return typeof(CqrsReflectionFallbackAttribute).Assembly .GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!; } + + /// + /// 模拟被错误声明为抽象类型的 generated registry。 + /// + private abstract class AbstractGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry + { + /// + /// 抽象 registry 即便具备注册逻辑,也不应被 direct 激活入口实例化。 + /// + /// 承载处理器映射的服务集合。 + /// 记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + } + } + + /// + /// 模拟缺少无参构造器的 generated registry。 + /// + private sealed class ConstructorArgumentNotificationHandlerRegistry : ICqrsHandlerRegistry + { + /// + /// 初始化一个只能通过额外参数构造的测试 registry。 + /// + /// 用于区分测试场景的占位参数。 + public ConstructorArgumentNotificationHandlerRegistry(string marker) + { + ArgumentNullException.ThrowIfNull(marker); + } + + /// + /// 此实现仅用于满足接口契约;本用例关注的是构造阶段失败后的异常语义。 + /// + /// 承载处理器映射的服务集合。 + /// 记录注册诊断的日志器。 + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(INotificationHandler), + typeof(GeneratedRegistryNotificationHandler)); + } + } }