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

编写Opc Ua NodeManager测试时,Moq NodeId包装类遇方法覆盖问题

OPC UA NodeManager测试用例问题排查

问题背景

编写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);

代码错误分析

  1. new virtual的多态失效问题
    使用new virtual定义的属性仅在NodeIdTestClass类型上下文里可见,属于“隐藏”基类属性而非重写。如果GetManagerHandle方法接收的参数类型是NodeId,那么执行时会直接调用基类NodeId的属性,Moq对NodeIdTestClass属性的设置完全不会生效。

  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 21:18:16