test(cqrs): 补充 registrar 激活失败分支测试

- 补充 generated registry 为抽象类型时的回退与抛错覆盖
- 补充 generated registry 缺少无参构造器时的回退与抛错覆盖
This commit is contained in:
gewuyou 2026-05-12 08:39:00 +08:00 committed by GeWuYou
parent 75e7785592
commit 5e9b903d0f
2 changed files with 251 additions and 0 deletions

View File

@ -138,6 +138,68 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
});
}
/// <summary>
/// 验证当 generated registry 是抽象类型时registrar 会记录告警并回退到反射扫描。
/// </summary>
[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);
}
/// <summary>
/// 验证当 generated registry 不暴露可访问无参构造器时registrar 会记录告警并回退到反射扫描。
/// </summary>
[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);
}
/// <summary>
/// 创建一个仅通过 generated registry 注册主 handler、并附带指定 fallback 元数据的程序集替身。
/// </summary>
@ -161,6 +223,24 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
return generatedAssembly;
}
/// <summary>
/// 创建一个只声明 generated registry attribute 的程序集替身,用于验证 registry 激活失败后的回退行为。
/// </summary>
/// <param name="assemblyName">用于日志与缓存键的程序集名。</param>
/// <param name="registryType">要暴露给 registrar 的 generated registry 类型。</param>
/// <returns>已完成基础接线的程序集 mock。</returns>
private static Mock<Assembly> CreateGeneratedRegistryAssembly(string assemblyName, Type registryType)
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns(assemblyName);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(registryType)]);
return generatedAssembly;
}
/// <summary>
/// 提取容器中针对 generated notification 注册的处理器实现类型。
/// </summary>
@ -259,4 +339,55 @@ internal sealed class CqrsHandlerRegistrarFallbackFailureTests
.Where(static log => log.Level == LogLevel.Warning)
.ToArray();
}
/// <summary>
/// 模拟 generated registry 被错误声明为抽象类型时的激活失败场景。
/// </summary>
private abstract class AbstractGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 抽象 registry 即便具备注册逻辑,也不应被运行时实例化。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
}
}
/// <summary>
/// 模拟 generated registry 缺少可访问无参构造器时的激活失败场景。
/// </summary>
private sealed class ConstructorArgumentNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 初始化一个只能通过额外参数构造的测试 registry。
/// </summary>
/// <param name="marker">用于区分测试场景的占位参数。</param>
public ConstructorArgumentNotificationHandlerRegistry(string marker)
{
ArgumentNullException.ThrowIfNull(marker);
}
/// <summary>
/// 此实现仅用于满足接口契约;本用例关注的是实例化失败前的回退行为。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
}
}
}

View File

@ -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)]));
}
/// <summary>
/// 验证 direct generated-registry 激活入口在 registry 为抽象类型时会抛出异常,并保留契约告警。
/// </summary>
[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<InvalidOperationException>(() =>
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);
});
}
/// <summary>
/// 验证 direct generated-registry 激活入口在 registry 缺少无参构造器时会抛出异常,并保留契约告警。
/// </summary>
[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<InvalidOperationException>(() =>
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);
});
}
/// <summary>
/// 验证当生成注册器元数据损坏时,运行时会记录告警并回退到反射扫描路径。
/// </summary>
@ -695,4 +764,55 @@ internal sealed class CqrsHandlerRegistrarTests
return typeof(CqrsReflectionFallbackAttribute).Assembly
.GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!;
}
/// <summary>
/// 模拟被错误声明为抽象类型的 generated registry。
/// </summary>
private abstract class AbstractGeneratedNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 抽象 registry 即便具备注册逻辑,也不应被 direct 激活入口实例化。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
}
}
/// <summary>
/// 模拟缺少无参构造器的 generated registry。
/// </summary>
private sealed class ConstructorArgumentNotificationHandlerRegistry : ICqrsHandlerRegistry
{
/// <summary>
/// 初始化一个只能通过额外参数构造的测试 registry。
/// </summary>
/// <param name="marker">用于区分测试场景的占位参数。</param>
public ConstructorArgumentNotificationHandlerRegistry(string marker)
{
ArgumentNullException.ThrowIfNull(marker);
}
/// <summary>
/// 此实现仅用于满足接口契约;本用例关注的是构造阶段失败后的异常语义。
/// </summary>
/// <param name="services">承载处理器映射的服务集合。</param>
/// <param name="logger">记录注册诊断的日志器。</param>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddTransient(
typeof(INotificationHandler<GeneratedRegistryNotification>),
typeof(GeneratedRegistryNotificationHandler));
}
}
}