You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对继承BaseAction的Action类单元测试并Mock基类?

针对MEF插件Action类的单元测试方案(Mock基类SendAction)

核心问题分析

你的场景中,子类Action覆写SendAction后会调用基类方法,但基类存在无法外部注入的内部对象,导致无法直接通过常规Mock工具控制基类方法的返回值。以下是几种可行的测试方案:


方案1:重构基类,拆分可测试依赖(优先推荐)

将基类中不可注入的内部对象抽象为接口,通过构造函数注入,从根源上解决可测试性问题,同时符合SOLID设计原则。

重构示例

// 新增抽象接口,封装基类内部的写入逻辑
public interface IActionWriter
{
    bool Write(string targetLocation, object data);
}

// 重构BaseAction,注入抽象依赖
public abstract class BaseAction : IBaseAction
{
    private readonly IActionWriter _writer;
    private readonly IDataValidator _validator;
    private readonly IActionLogger _logger;

    // 通过构造函数注入所有依赖
    public BaseAction(IActionWriter writer, IDataValidator validator, IActionLogger logger)
    {
        _writer = writer;
        _validator = validator;
        _logger = logger;
    }

    public virtual bool SendAction(ActionType type, object data)
    {
        // 原校验、日志逻辑改为依赖注入接口
        if (!_validator.Check(data))
        {
            _logger.RecordError("数据校验失败");
            return false;
        }
        return _writer.Write(GetTargetLocation(type), data);
    }

    // 子类需实现的抽象方法,定义不同Action的目标位置
    protected abstract string GetTargetLocation(ActionType type);
}

测试示例

[Test]
public void ActionA_SendAction_ReturnsTrue_WhenBaseLogicSucceeds()
{
    // Arrange
    var mockWriter = new Mock<IActionWriter>();
    mockWriter.Setup(w => w.Write(It.IsAny<string>(), It.IsAny<object>())).Returns(true);
    var mockValidator = new Mock<IDataValidator>();
    mockValidator.Setup(v => v.Check(It.IsAny<object>())).Returns(true);
    var mockLogger = new Mock<IActionLogger>();

    var actionA = new ActionA(mockWriter.Object, mockValidator.Object, mockLogger.Object);

    // Act
    var result = actionA.SendAction(ActionType.TypeA, new TestPayload());

    // Assert
    Assert.IsTrue(result);
    // 验证子类自定义逻辑是否触发了预期的基类调用
    mockWriter.Verify(w => w.Write("LocationA", It.IsAny<object>()), Times.Once);
}

方案2:使用隔离框架绕过基类内部依赖(无法重构时使用)

如果无法修改现有基类代码,可使用支持拦截非虚方法、内部对象的商业隔离框架(如TypeMock Isolator、JustMock),直接Mock基类的SendAction方法或内部对象的行为。

示例(JustMock)

[Test]
public void ActionA_SendAction_ReturnsFalse_WhenBaseMethodMocked()
{
    // Arrange
    var actionA = new ActionA();
    // 直接拦截基类的SendAction方法,强制返回false
    Mock.NonPublic.Arrange<bool>(actionA, "SendAction", ActionType.TypeA, It.IsAny<object>()).Returns(false);

    // Act
    var result = actionA.SendAction(ActionType.TypeA, new TestPayload());

    // Assert
    Assert.IsFalse(result);
    // 验证子类自定义逻辑是否执行(例如检查日志、状态变化等副作用)
}

方案3:创建测试专用子类Wrapper

在测试项目中编写继承自目标Action类的测试子类,覆写SendAction方法,跳过基类调用或直接返回预设值,同时保留子类的自定义逻辑。

测试子类示例

// 测试项目中的专用子类
public class TestableActionA : ActionA
{
    public bool MockBaseResult { get; set; } = true;

    public override bool SendAction(ActionType type, object data)
    {
        // 执行ActionA的自定义逻辑(可直接调用原方法或复制逻辑)
        ExecuteCustomPreLogic();
        // 跳过基类调用,返回Mock值
        return MockBaseResult;
    }

    // 暴露原Action的私有/保护自定义逻辑,便于测试验证
    public void ExecuteCustomPreLogic()
    {
        base.ExecuteCustomPreLogic();
    }
}

测试示例

[Test]
public void ActionA_SendAction_ReturnsMockedValue()
{
    // Arrange
    var testableAction = new TestableActionA();
    testableAction.MockBaseResult = false;

    // Act
    var result = testableAction.SendAction(ActionType.TypeA, new TestPayload());

    // Assert
    Assert.IsFalse(result);
    // 验证自定义逻辑是否执行(例如检查是否生成了预期的临时文件、状态标记)
}

方案4:反射替换基类内部对象(临时应急方案)

通过反射访问基类的私有字段,替换为Mock实例。此方案依赖基类内部实现细节,重构后测试易失效,仅作为临时解决办法。

示例

[Test]
public void ActionA_SendAction_ReturnsTrue_WhenInternalObjectMocked()
{
    // Arrange
    var actionA = new ActionA();
    // 通过反射获取基类私有字段
    var internalWriterField = typeof(BaseAction).GetField("_internalWriter", BindingFlags.NonPublic | BindingFlags.Instance);
    // 创建Mock对象替换原内部对象
    var mockWriter = new Mock<IActionWriter>();
    mockWriter.Setup(w => w.Write(It.IsAny<string>(), It.IsAny<object>())).Returns(true);
    internalWriterField.SetValue(actionA, mockWriter.Object);

    // Act
    var result = actionA.SendAction(ActionType.TypeA, new TestPayload());

    // Assert
    Assert.IsTrue(result);
}

方案选择建议

  1. 优先选择方案1,重构基类拆分依赖,这是长期可维护的最优解;
  2. 若无法重构代码,可根据预算选择方案2(商业框架),或选择方案3(测试子类);
  3. 方案4仅作为临时应急手段,不推荐长期使用。

内容的提问来源于stack exchange,提问作者MckPT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 12:05:38