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

Moq调用Verify校验入参时抛出NullReferenceException异常

Moq执行Verify校验时抛出NullReferenceException的解决方法

问题背景

使用Moq框架对同步流程做单元测试时,业务逻辑执行正常,但在调用Mock对象的Verify方法做调用校验时触发空引用异常。

待测试业务代码

(注:原贴代码存在几处语法笔误,包括构造函数参数缺逗号、elseif应为else if、Ilogger拼写错误,实际运行时需修正)

public class SyncManager
{
    private IPubHttpClient _pubHttpClient;
    private ILogService _logService;
    private ILogger<SyncManager> _logger;

    public SyncManager(IPubHttpClient pubClient, ILogService logService, ILogger<SyncManager> logger)
    {
        _pubHttpClient = pubClient;
        _logService = logService;
        _logger = logger;
    }

    public async Task Sync()
    {
        var syncStatus = SyncStatus.Error;

        // get logs
        var logs = await _logService.GetLogs();

        foreach (var log in logs)
        {
            if (!string.IsNullOrEmpty(log.CostCode))
               syncStatus = await GetAndSendCost(log);
            else
               syncStatus = await GetAndSendSort(log);         
        }
    }

    private async Task<SyncStatus> GetAndSendCost(Log log)
    {
        var cost = new Cost
        {
            CostCode = log.CostCode,
            CostName = log.Description,
            Active = log.Active
        };

        await _pubHttpClient.Push(new EventModel { Cost = cost, MessageType = log.Type.GetDescription() });

        return SyncStatus.Success;
    }

    private async Task<SyncStatus> GetAndSendSort(Log log)
    {
        var sort = new Sort
        {
            SortCode = log.SortCode,
            SortName = log.Description,
            Active = log.Active
        };

        await _pubHttpClient.Push(new EventModel { Sort = sort, MessageType = log.Type.GetDescription() });

        return SyncStatus.Success;
    }
}

public class Log
{
    public long Id { get; set; }
    public string SortCode { get; set; }
    public string CostCode { get; set; }
    public string Description { get; set; }
    public string Active { get; set; }
    public AuditType Type { get; set; }
}

public class EventModel 
{
    public Cost Cost { get; set; }
    public Sort Sort { get; set; }
    public string MessageType { get; set; }
}

public enum AuditType
{
    [Description("CREATE")]
    Create = 0,
    [Description("UPDATE")]
    Update = 1,
    [Description("DELETE")]
    Delete = 2
}

public static class EnumExtensions
{
    public static string GetDescription(this Enum enumValue)
    {
        return enumValue.GetType()
                   .GetMember(enumValue.ToString())
                   .First()
                   .GetCustomAttribute<DescriptionAttribute>()?
                   .Description ?? string.Empty;
    }
}

原单元测试代码

public class SyncManagerTests
{
    public readonly Mock<IPubHttpClient> _pubClientMock = new();
    public readonly Mock<ILogService> _logServiceMock = new();

    [Fact]
    public async Task Should_Sync()
    {
        var mockedCost = new Cost { Active = Status.Active, CostCode = "0000", CostName = "UNIT TEST" };
        var mockedSort = new Sort { Active = Status.Active, SortCode = "0001", SortName = "UNIT TEST" };
        var mockedLogs = new List<Log> { 
            new Log { CostCode = mockedCost.CostCode, Description = mockedCost.CostName, Active = mockedCost.Active, Id = 1 },
            new Log { SortCode = mockedSort.SortCode, Description = mockedSort.CostName, Active = mockedSort.Active, Id = 2 },
        };

        _logServiceMock.Setup(s => s.GetLogs()).ReturnsAsync(mockedLogs).Verifiable();
        _pubClientMock.Setup(p => p.Push(It.Is<EventModel>(x => x.Cost == mockedCost && x.MessageType == "CREATE"))).Returns(Task.CompletedTask).Verifiable();

        var syncManager = new SyncManager(_pubClientMock.Object, _logServiceMock.Object, Mock.Of<ILogger<SyncManager>>());

        await syncManager.Sync();

        _pubClientMock.Verify(p => p.Push(It.Is<EventModel>(
            x => x.Cost.CostName == mockedCost.CostName
            && x.Cost.CostCode == mockedCost.CostCode
            && x.Cost.Active == mockedCost.Active
            && x.MessageType == "CREATE")));
    }
}

异常表现

  • 逐行调试Sync()方法,业务执行逻辑完全符合预期,运行过程中生成的EventModel实例属性值全部正确
  • 执行_pubClientMock.Verify()时抛出System.NullReferenceException,提示校验表达式中x.Cost为NULL

根本原因

Moq执行Verify时,会遍历所有被拦截到的Push方法调用,逐个执行传入的匹配表达式判断是否符合校验规则。
测试用例中mock了2条日志数据:

  1. 第一条日志带CostCode,会调用GetAndSendCost,传入Push的EventModel实例带Cost属性值,Sort属性为null
  2. 第二条日志不带CostCode,会调用GetAndSendSort,传入Push的EventModel实例带Sort属性值,Cost属性为null

当Moq遍历到第二条调用记录时,会执行匹配表达式x => x.Cost.CostName == ...,此时x.Cost就是null,直接访问CostName属性自然会抛出空引用异常。
业务代码调试时不会触发该问题,是因为业务逻辑本身不会在Sort分支访问Cost属性,但Moq的校验逻辑会把所有调用都传入匹配表达式执行,没有区分调用场景。

另外原测试代码还存在两个隐藏问题:

  • Setup方法中用x.Cost == mockedCost做匹配是引用相等判断,业务逻辑中每次都会new一个新的Cost实例,永远不会和测试中定义的mockedCost是同一个对象,该Setup规则永远不会命中
  • mock的第二条日志数据没有显式赋值Type属性,枚举默认值为0即Create,和测试预期的MessageType="CREATE"存在逻辑偏差

修复方案

  1. 给Verify的匹配表达式增加空值判断,先判断x.Cost不为空再访问子属性,避免遍历到Sort分支的调用时触发空引用,同时可以显式指定校验调用次数为1次,确保Cost分支确实被调用:
    _pubClientMock.Verify(p => p.Push(It.Is<EventModel>(
        x => x.Cost != null
        && x.Cost.CostName == mockedCost.CostName
        && x.Cost.CostCode == mockedCost.CostCode
        && x.Cost.Active == mockedCost.Active
        && x.MessageType == "CREATE")), Times.Once);
    
  2. 修正Setup方法的匹配规则,去掉引用相等判断,改用属性值匹配,和Verify逻辑保持一致,确保Setup能正确命中。
  3. 补全mock日志数据的Type属性显式赋值,避免枚举默认值导致测试数据和预期不符。
  4. 如果需要校验Sort分支的调用逻辑,新增对应的Verify语句时,同样需要先判断x.Sort不为空再访问子属性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 23:27:28