编写Opc Ua NodeManager测试时,Moq NodeId包装类遇方法覆盖问题
问题背景
编写OPC UA NodeManager测试用例时,由于依赖的Opc.Ua.NodeId类属性无virtual修饰、不可重写,且仅提供get访问器,无法直接用Moq模拟。于是创建了继承自NodeId的包装类NodeIdTestClass,通过new virtual定义属性试图实现可模拟,但运行时出现异常。相关代码如下:
包装类代码
public class NodeIdTestClass : NodeId { public NodeIdTestClass() { } public new virtual object Identifier { get => base.Identifier; } public new virtual ushort NamespaceIndex { get => base.NamespaceIndex; } public new virtual bool IsNullNodeId { get => base.IsNullNodeId; } }
测试代码
//Arrange var nodeIdMock = new Mock<NodeIdTestClass>() { CallBase = true }; nodeIdMock.Setup(x => x.Identifier).Returns(nodeIdMock.Object.Identifier); nodeIdMock.Setup(x => x.NamespaceIndex).Returns(1); nodeIdMock.Setup(x => x.IsNullNodeId).Returns(false); _nodemanager.SetNamespaces(new string[] { "0", "1", "2" }); //Act var result = _nodemanager.GetManagerHandle(nodeIdMock.Object);
代码错误分析
new virtual的多态失效问题
使用new virtual定义的属性仅在NodeIdTestClass类型上下文里可见,属于“隐藏”基类属性而非重写。如果GetManagerHandle方法接收的参数类型是NodeId,那么执行时会直接调用基类NodeId的属性,Moq对NodeIdTestClass属性的设置完全不会生效。Identifier的Setup逻辑错误nodeIdMock.Setup(x => x.Identifier).Returns(nodeIdMock.Object.Identifier);这行代码会触发循环依赖:Setup过程中需要获取Mock对象的Identifier属性,而此时Mock尚未完成初始化,极大概率引发空引用或栈溢出类异常。
修复方案
方案1:接口抽象+适配器(推荐)
通过定义抽象接口封装NodeId的所需属性,再创建适配器类实现接口并包装NodeId实例,彻底规避基类不可重写的限制:
// 定义抽象接口 public interface INodeIdWrapper { object Identifier { get; } ushort NamespaceIndex { get; } bool IsNullNodeId { get; } } // 适配器类 public class NodeIdAdapter : INodeIdWrapper { private readonly NodeId _nodeId; public NodeIdAdapter(NodeId nodeId) { _nodeId = nodeId; } public object Identifier => _nodeId.Identifier; public ushort NamespaceIndex => _nodeId.NamespaceIndex; public bool IsNullNodeId => _nodeId.IsNullNodeId; }
修改NodeManager.GetManagerHandle的参数类型为INodeIdWrapper(若无法修改原代码,可在测试中用适配器包装实际NodeId或Mock接口)。测试代码示例:
// Arrange var nodeIdMock = new Mock<INodeIdWrapper>(); nodeIdMock.Setup(x => x.Identifier).Returns("test-node-id"); // 直接指定测试值 nodeIdMock.Setup(x => x.NamespaceIndex).Returns((ushort)1); nodeIdMock.Setup(x => x.IsNullNodeId).Returns(false); _nodemanager.SetNamespaces(new string[] { "0", "1", "2" }); // Act var result = _nodemanager.GetManagerHandle(nodeIdMock.Object);
方案2:修正现有包装类的测试逻辑(仅限特定场景)
若GetManagerHandle的参数类型就是NodeIdTestClass,可调整测试代码:
// Arrange var nodeIdMock = new Mock<NodeIdTestClass>() { CallBase = false }; // 关闭CallBase,直接模拟属性 nodeIdMock.Setup(x => x.Identifier).Returns("test-identifier"); // 直接返回测试值,避免循环引用 nodeIdMock.Setup(x => x.NamespaceIndex).Returns((ushort)1); nodeIdMock.Setup(x => x.IsNullNodeId).Returns(false); _nodemanager.SetNamespaces(new string[] { "0", "1", "2" }); // Act var result = _nodemanager.GetManagerHandle(nodeIdMock.Object);
注意:此方案仅适用于NodeManager内部不会将NodeIdTestClass转为NodeId类型的场景,否则仍会触发基类属性逻辑。
内容的提问来源于stack exchange,提问作者Amit Kumar

