GFramework/GFramework.Core.Tests/Command/CommandExecutorTests.cs
gewuyou ffb0a8aff5 fix(core): 收窄 legacy bridge 上下文回退异常边界
- 修复 LegacyCqrsDispatchHelper 仅在上下文缺失时回退,避免吞掉真实 InvalidOperationException

- 补充 CommandExecutor 与 QueryExecutor 相关回归测试,覆盖 fallback 与异常冒泡语义

- 更新 cqrs-rewrite 跟踪与追踪文档,记录 PR #334 本轮复核与验证结果
2026-05-07 20:35:47 +08:00

278 lines
9.3 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.

// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using GFramework.Core.Command;
using GFramework.Core.Rule;
using GFramework.Core.Tests.Architectures;
namespace GFramework.Core.Tests.Command;
/// <summary>
/// CommandBus类的单元测试
/// 测试内容包括:
/// - Send方法执行命令
/// - Send方法处理null命令
/// - Send方法带返回值返回值
/// - Send方法带返回值处理null命令
/// - SendAsync方法执行异步命令
/// - SendAsync方法处理null异步命令
/// - SendAsync方法带返回值返回值
/// - SendAsync方法带返回值处理null异步命令
/// </summary>
[TestFixture]
public class CommandExecutorTests
{
[SetUp]
public void SetUp()
{
_commandExecutor = new CommandExecutor();
}
private CommandExecutor _commandExecutor = null!;
/// <summary>
/// 测试Send方法执行命令
/// </summary>
[Test]
public void Send_Should_Execute_Command()
{
var input = new TestCommandInput { Value = 42 };
var command = new TestCommand(input);
Assert.DoesNotThrow(() => _commandExecutor.Send(command));
Assert.That(command.Executed, Is.True);
Assert.That(command.ExecutedValue, Is.EqualTo(42));
}
/// <summary>
/// 测试Send方法处理null命令时抛出ArgumentNullException异常
/// </summary>
[Test]
public void Send_WithNullCommand_Should_ThrowArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _commandExecutor.Send(null!));
}
/// <summary>
/// 测试Send方法带返回值正确返回值
/// </summary>
[Test]
public void Send_WithResult_Should_Return_Value()
{
var input = new TestCommandInput { Value = 100 };
var command = new TestCommandWithResult(input);
var result = _commandExecutor.Send(command);
Assert.That(command.Executed, Is.True);
Assert.That(result, Is.EqualTo(200));
}
/// <summary>
/// 测试Send方法带返回值处理null命令时抛出ArgumentNullException异常
/// </summary>
[Test]
public void Send_WithResult_AndNullCommand_Should_ThrowArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _commandExecutor.Send<int>(null!));
}
/// <summary>
/// 验证当 legacy 命令没有可用上下文时,会安全回退到本地直接执行路径。
/// </summary>
[Test]
public void Send_Should_Fall_Back_To_Legacy_Execution_When_Context_IsMissing()
{
var runtime = new RecordingCqrsRuntime();
var executor = new CommandExecutor(runtime);
var command = new MissingContextLegacyCommand();
executor.Send(command);
Assert.Multiple(() =>
{
Assert.That(command.Executed, Is.True);
Assert.That(runtime.LastRequest, Is.Null);
});
}
/// <summary>
/// 验证非“缺上下文”类型的 <see cref="InvalidOperationException" /> 不会被 bridge 回退逻辑误吞掉。
/// </summary>
[Test]
public void Send_Should_Propagate_InvalidOperationException_When_ContextAware_Target_Throws_Unexpected_Error()
{
var runtime = new RecordingCqrsRuntime();
var executor = new CommandExecutor(runtime);
var command = new ThrowingLegacyCommand();
Assert.That(
() => executor.Send(command),
Throws.InvalidOperationException.With.Message.EqualTo(ThrowingLegacyCommand.ExceptionMessage));
Assert.That(runtime.LastRequest, Is.Null);
}
/// <summary>
/// 验证 legacy 同步命令桥接会在线程池上等待 runtime
/// 避免直接继承调用方当前的同步上下文。
/// </summary>
[Test]
public void Send_Should_Bridge_Through_Runtime_Without_Reusing_Caller_SynchronizationContext()
{
var runtime = new RecordingCqrsRuntime();
var executor = new CommandExecutor(runtime);
var command = new ContextAwareLegacyCommand();
var expectedContext = new TestArchitectureContextBaseStub();
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
var originalContext = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(new TestLegacySynchronizationContext());
executor.Send(command);
Assert.Multiple(() =>
{
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandDispatchRequest>());
Assert.That(runtime.ObservedSynchronizationContextType, Is.Null);
});
}
finally
{
SynchronizationContext.SetSynchronizationContext(originalContext);
}
}
/// <summary>
/// 验证 legacy 带返回值命令桥接也会保留上下文注入与返回值语义。
/// </summary>
[Test]
public void Send_WithResult_Should_Bridge_Through_Runtime_And_Preserve_Context()
{
var runtime = new RecordingCqrsRuntime(static _ => 123);
var executor = new CommandExecutor(runtime);
var command = new ContextAwareLegacyCommandWithResult(123);
var expectedContext = new TestArchitectureContextBaseStub();
((GFramework.Core.Abstractions.Rule.IContextAware)command).SetContext(expectedContext);
var result = executor.Send(command);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(123));
Assert.That(runtime.LastRequest, Is.TypeOf<GFramework.Core.Cqrs.LegacyCommandResultDispatchRequest>());
Assert.That(command.ObservedContext, Is.SameAs(expectedContext));
});
}
/// <summary>
/// 测试SendAsync方法执行异步命令
/// </summary>
[Test]
public async Task SendAsync_Should_Execute_AsyncCommand()
{
var input = new TestCommandInput { Value = 42 };
var command = new TestAsyncCommand(input);
await _commandExecutor.SendAsync(command);
Assert.That(command.Executed, Is.True);
Assert.That(command.ExecutedValue, Is.EqualTo(42));
}
/// <summary>
/// 测试SendAsync方法处理null异步命令时抛出ArgumentNullException异常
/// </summary>
[Test]
public void SendAsync_WithNullCommand_Should_ThrowArgumentNullException()
{
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync(null!));
}
/// <summary>
/// 测试SendAsync方法带返回值正确返回值
/// </summary>
[Test]
public async Task SendAsync_WithResult_Should_Return_Value()
{
var input = new TestCommandInput { Value = 100 };
var command = new TestAsyncCommandWithResult(input);
var result = await _commandExecutor.SendAsync(command);
Assert.That(command.Executed, Is.True);
Assert.That(result, Is.EqualTo(200));
}
/// <summary>
/// 测试SendAsync方法带返回值处理null异步命令时抛出ArgumentNullException异常
/// </summary>
[Test]
public void SendAsync_WithResult_AndNullCommand_Should_ThrowArgumentNullException()
{
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync<int>(null!));
}
/// <summary>
/// 为同步 bridge 测试提供最小架构上下文替身。
/// </summary>
private sealed class TestArchitectureContextBaseStub : TestArchitectureContextBase
{
}
/// <summary>
/// 用于验证缺少上下文时仍会走本地 fallback 的测试命令。
/// </summary>
private sealed class MissingContextLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
{
/// <summary>
/// 获取命令是否已经执行。
/// </summary>
public bool Executed { get; private set; }
/// <inheritdoc />
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
ArgumentNullException.ThrowIfNull(context);
}
/// <inheritdoc />
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
{
throw new InvalidOperationException("Architecture context has not been set. Call SetContext before accessing the context.");
}
/// <inheritdoc />
public void Execute()
{
Executed = true;
}
}
/// <summary>
/// 用于验证 bridge 上下文解析不会吞掉意外运行时错误的测试命令。
/// </summary>
private sealed class ThrowingLegacyCommand : GFramework.Core.Abstractions.Rule.IContextAware, GFramework.Core.Abstractions.Command.ICommand
{
internal const string ExceptionMessage = "Unexpected context failure.";
/// <inheritdoc />
public void SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
ArgumentNullException.ThrowIfNull(context);
}
/// <inheritdoc />
public GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext()
{
throw new InvalidOperationException(ExceptionMessage);
}
/// <inheritdoc />
public void Execute()
{
}
}
}