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条日志数据:
- 第一条日志带CostCode,会调用
GetAndSendCost,传入Push的EventModel实例带Cost属性值,Sort属性为null - 第二条日志不带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"存在逻辑偏差
修复方案
- 给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); - 修正Setup方法的匹配规则,去掉引用相等判断,改用属性值匹配,和Verify逻辑保持一致,确保Setup能正确命中。
- 补全mock日志数据的
Type属性显式赋值,避免枚举默认值导致测试数据和预期不符。 - 如果需要校验Sort分支的调用逻辑,新增对应的Verify语句时,同样需要先判断x.Sort不为空再访问子属性。
内容的提问来源于stack exchange,提问作者Schoof
相关产品推荐
相关产品推荐

