如何对继承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,重构基类拆分依赖,这是长期可维护的最优解;
- 若无法重构代码,可根据预算选择方案2(商业框架),或选择方案3(测试子类);
- 方案4仅作为临时应急手段,不推荐长期使用。
内容的提问来源于stack exchange,提问作者MckPT
相关产品推荐
相关产品推荐

