GFramework/GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs
GeWuYou e2001766cb feat(arch): 添加架构上下文实现及完整测试
- 实现 ArchitectureContext 类,提供对系统、模型、工具等组件的访问
- 集成 CQRS runtime,支持命令、查询、事件的执行管理
- 添加服务缓存机制,优化容器解析性能
- 实现并发安全的 CQRS runtime 懒加载
- 提供同步和异步的请求处理方法
- 支持优先级排序的服务实例获取
- 添加完整的单元测试覆盖构造函数、查询、命令、事件等功能
- 配置测试项目依赖和全局引用
- 实现共享的 CQRS 测试运行时支持
2026-04-15 15:57:08 +08:00

512 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Abstractions.Cqrs;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Command;
using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// ArchitectureContext类的单元测试
/// 测试内容包括:
/// - 构造函数参数验证所有5个参数
/// - 构造函数空参数异常
/// - SendQuery方法 - 正常查询发送
/// - SendQuery方法 - 空查询异常
/// - SendCommand方法 - 正常命令发送
/// - SendCommand方法 - 空命令异常
/// - SendCommand_WithResult方法 - 正常命令发送
/// - SendCommand_WithResult方法 - 空命令异常
/// - SendEvent方法 - 正常事件发送
/// - SendEvent_WithInstance方法 - 正常事件发送
/// - SendEvent_WithInstance方法 - 空事件异常
/// - GetSystem方法 - 获取已注册系统
/// - GetSystem方法 - 获取未注册系统时抛出异常
/// - GetModel方法 - 获取已注册模型
/// - GetModel方法 - 获取未注册模型时抛出异常
/// - GetUtility方法 - 获取已注册工具
/// - GetUtility方法 - 获取未注册工具时抛出异常
/// - GetEnvironment方法 - 获取环境对象
/// </summary>
[TestFixture]
public class ArchitectureContextTests
{
[SetUp]
public void SetUp()
{
// 初始化 LoggerFactoryResolver 以支持 MicrosoftDiContainer
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
_container = new MicrosoftDiContainer();
// 直接初始化 logger 字段
var loggerField = typeof(MicrosoftDiContainer).GetField("_logger",
BindingFlags.NonPublic | BindingFlags.Instance);
loggerField?.SetValue(_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(ArchitectureContextTests)));
// 创建服务实例
_eventBus = new EventBus();
_commandBus = new CommandExecutor();
_queryBus = new QueryExecutor();
_asyncQueryBus = new AsyncQueryExecutor();
_environment = new DefaultEnvironment();
// 将服务注册到容器
_container.RegisterPlurality(_eventBus);
_container.RegisterPlurality(_commandBus);
_container.RegisterPlurality(_queryBus);
_container.RegisterPlurality(_asyncQueryBus);
_container.RegisterPlurality(_environment);
_context = new ArchitectureContext(_container);
}
private ArchitectureContext? _context;
private MicrosoftDiContainer? _container;
private EventBus? _eventBus;
private CommandExecutor? _commandBus;
private QueryExecutor? _queryBus;
private AsyncQueryExecutor? _asyncQueryBus;
private DefaultEnvironment? _environment;
/// <summary>
/// 测试构造函数在所有参数都有效时不应抛出异常
/// </summary>
[Test]
public void Constructor_Should_NotThrow_When_AllParameters_AreValid()
{
Assert.That(() => new ArchitectureContext(_container!), Throws.Nothing);
}
/// <summary>
/// 测试构造函数在 container 为 null 时应抛出 ArgumentNullException
/// </summary>
[Test]
public void Constructor_Should_Throw_When_Container_IsNull()
{
Assert.That(() => new ArchitectureContext(null!), Throws.ArgumentNullException);
}
/// <summary>
/// 测试构造函数在Container为null时应抛出ArgumentNullException
/// </summary>
[Test]
public void Constructor_Should_ThrowArgumentNullException_When_Container_IsNull()
{
Assert.That(() => new ArchitectureContext(null!),
Throws.ArgumentNullException.With.Property("ParamName").EqualTo("container"));
}
/// <summary>
/// 测试SendQuery方法在查询有效时返回正确结果
/// </summary>
[Test]
public void SendQuery_Should_ReturnResult_When_Query_IsValid()
{
var testQuery = new TestQueryV2 { Result = 42 };
var result = _context!.SendQuery(testQuery);
Assert.That(result, Is.EqualTo(42));
}
/// <summary>
/// 测试SendQuery方法在查询为null时应抛出ArgumentNullException
/// </summary>
[Test]
public void SendQuery_Should_ThrowArgumentNullException_When_Query_IsNull()
{
// 明确指定调用旧的 IQuery<int> 重载
Assert.That(() => _context!.SendQuery((IQuery<int>)null!),
Throws.ArgumentNullException.With.Property("ParamName").EqualTo("query"));
}
/// <summary>
/// 测试SendCommand方法在命令有效时正确执行
/// </summary>
[Test]
public void SendCommand_Should_ExecuteCommand_When_Command_IsValid()
{
var testCommand = new TestCommandV2();
Assert.That(() => _context!.SendCommand(testCommand), Throws.Nothing);
Assert.That(testCommand.Executed, Is.True);
}
/// <summary>
/// 测试SendCommand方法在命令为null时应抛出ArgumentNullException
/// </summary>
[Test]
public void SendCommand_Should_ThrowArgumentNullException_When_Command_IsNull()
{
Assert.That(() => _context!.SendCommand(null!),
Throws.ArgumentNullException.With.Property("ParamName").EqualTo("command"));
}
/// <summary>
/// 测试SendCommand方法带返回值在命令有效时返回正确结果
/// </summary>
[Test]
public void SendCommand_WithResult_Should_ReturnResult_When_Command_IsValid()
{
var testCommand = new TestCommandWithResultV2 { Result = 123 };
var result = _context!.SendCommand(testCommand);
Assert.That(result, Is.EqualTo(123));
}
/// <summary>
/// 测试SendCommand方法带返回值在命令为null时应抛出ArgumentNullException
/// </summary>
[Test]
public void SendCommand_WithResult_Should_ThrowArgumentNullException_When_Command_IsNull()
{
Assert.That(() => _context!.SendCommand((ICommand<int>)null!),
Throws.ArgumentNullException.With.Property("ParamName").EqualTo("command"));
}
/// <summary>
/// 测试SendEvent方法在事件类型有效时正确发送事件
/// </summary>
[Test]
public void SendEvent_Should_SendEvent_When_EventType_IsValid()
{
var eventReceived = false;
_context!.RegisterEvent<TestEventV2>(_ => eventReceived = true);
_context.SendEvent<TestEventV2>();
Assert.That(eventReceived, Is.True);
}
/// <summary>
/// 测试SendEvent方法带实例在事件实例有效时正确发送事件
/// </summary>
[Test]
public void SendEvent_WithInstance_Should_SendEvent_When_EventInstance_IsValid()
{
var eventReceived = false;
var testEvent = new TestEventV2();
_context!.RegisterEvent<TestEventV2>(_ => eventReceived = true);
_context.SendEvent(testEvent);
Assert.That(eventReceived, Is.True);
}
/// <summary>
/// 测试SendEvent方法带实例在事件实例为null时应抛出ArgumentNullException
/// </summary>
[Test]
public void SendEvent_WithInstance_Should_ThrowArgumentNullException_When_EventInstance_IsNull()
{
Assert.That(() => _context!.SendEvent<TestEventV2>(null!),
Throws.ArgumentNullException.With.Property("ParamName").EqualTo("e"));
}
/// <summary>
/// 测试GetSystem方法在系统已注册时返回注册的系统
/// </summary>
[Test]
public void GetSystem_Should_ReturnRegisteredSystem_When_SystemIsRegistered()
{
var testSystem = new TestSystemV2();
_container!.RegisterPlurality(testSystem);
var result = _context!.GetSystem<TestSystemV2>();
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.SameAs(testSystem));
}
/// <summary>
/// 测试GetSystem方法在系统未注册时应抛出 InvalidOperationException
/// </summary>
[Test]
public void GetSystem_Should_ThrowInvalidOperationException_When_SystemIsNotRegistered()
{
Assert.That(() => _context!.GetSystem<TestSystemV2>(),
Throws.InvalidOperationException);
}
/// <summary>
/// 测试GetModel方法在模型已注册时返回注册的模型
/// </summary>
[Test]
public void GetModel_Should_ReturnRegisteredModel_When_ModelIsRegistered()
{
var testModel = new TestModelV2();
_container!.RegisterPlurality(testModel);
var result = _context!.GetModel<TestModelV2>();
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.SameAs(testModel));
}
/// <summary>
/// 测试GetModel方法在模型未注册时应抛出 InvalidOperationException
/// </summary>
[Test]
public void GetModel_Should_ThrowInvalidOperationException_When_ModelIsNotRegistered()
{
Assert.That(() => _context!.GetModel<TestModelV2>(),
Throws.InvalidOperationException);
}
/// <summary>
/// 测试GetUtility方法在工具已注册时返回注册的工具
/// </summary>
[Test]
public void GetUtility_Should_ReturnRegisteredUtility_When_UtilityIsRegistered()
{
var testUtility = new TestUtilityV2();
_container!.RegisterPlurality(testUtility);
var result = _context!.GetUtility<TestUtilityV2>();
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.SameAs(testUtility));
}
/// <summary>
/// 测试GetUtility方法在工具未注册时应抛出 InvalidOperationException
/// </summary>
[Test]
public void GetUtility_Should_ThrowInvalidOperationException_When_UtilityIsNotRegistered()
{
Assert.That(() => _context!.GetUtility<TestUtilityV2>(),
Throws.InvalidOperationException);
}
/// <summary>
/// 测试GetEnvironment方法返回环境实例
/// </summary>
[Test]
public void GetEnvironment_Should_Return_EnvironmentInstance()
{
var environment = _context!.GetEnvironment();
Assert.That(environment, Is.Not.Null);
Assert.That(environment, Is.InstanceOf<IEnvironment>());
}
/// <summary>
/// 测试 CQRS runtime 在并发首次访问时只会从容器解析一次。
/// </summary>
[Test]
public async Task SendRequestAsync_Should_ResolveCqrsRuntime_OnlyOnce_When_AccessedConcurrently()
{
using var startGate = new ManualResetEventSlim(false);
using var allowResolutionToComplete = new ManualResetEventSlim(false);
var resolutionCallCount = 0;
var runtime = new Mock<ICqrsRuntime>(MockBehavior.Strict);
var container = new Mock<IIocContainer>(MockBehavior.Strict);
runtime.Setup(mockRuntime => mockRuntime.SendAsync(
It.IsAny<IArchitectureContext>(),
It.IsAny<IRequest<int>>(),
It.IsAny<CancellationToken>()))
.Returns(new ValueTask<int>(42));
container.Setup(mockContainer => mockContainer.Get<ICqrsRuntime>())
.Returns(() =>
{
Interlocked.Increment(ref resolutionCallCount);
allowResolutionToComplete.Wait();
return runtime.Object;
});
var context = new ArchitectureContext(container.Object);
var requests = Enumerable.Range(0, 16)
.Select(_ => Task.Run(async () =>
{
startGate.Wait();
return await context.SendRequestAsync(new TestCqrsRequest());
}))
.ToArray();
startGate.Set();
Assert.That(
SpinWait.SpinUntil(() => Volatile.Read(ref resolutionCallCount) > 0, TimeSpan.FromSeconds(1)),
Is.True,
"Expected at least one CQRS runtime resolution attempt.");
// 留出一个短暂窗口,让并发首次访问都在 runtime 尚未发布前抵达同一初始化点。
await Task.Delay(50);
allowResolutionToComplete.Set();
var responses = await Task.WhenAll(requests);
Assert.That(responses, Has.All.EqualTo(42));
Assert.That(resolutionCallCount, Is.EqualTo(1));
container.Verify(mockContainer => mockContainer.Get<ICqrsRuntime>(), Times.Once);
runtime.Verify(
mockRuntime => mockRuntime.SendAsync(
It.IsAny<IArchitectureContext>(),
It.IsAny<IRequest<int>>(),
It.IsAny<CancellationToken>()),
Times.Exactly(requests.Length));
}
private sealed class TestCqrsRequest : IRequest<int>
{
}
}
#region Test Classes
public class TestSystemV2 : ISystem
{
private IArchitectureContext _context = null!;
public int Id { get; init; }
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
public void Initialize()
{
}
public void Destroy()
{
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
}
public class TestModelV2 : IModel
{
private IArchitectureContext _context = null!;
public int Id { get; init; }
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
public void Initialize()
{
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void Destroy()
{
}
}
public class TestUtilityV2 : IUtility
{
private IArchitectureContext _context = null!;
public int Id { get; init; }
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
public class TestQueryV2 : IQuery<int>
{
private IArchitectureContext _context = null!;
public int Result { get; init; }
public int Do()
{
return Result;
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
public class TestCommandV2 : ICommand
{
private IArchitectureContext _context = null!;
public bool Executed { get; private set; }
public void Execute()
{
Executed = true;
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
public class TestCommandWithResultV2 : ICommand<int>
{
private IArchitectureContext _context = null!;
public int Result { get; init; }
public int Execute()
{
return Result;
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
public class TestEventV2
{
public int Data { get; init; }
}
#endregion