使用AutoFixture和NSubstitute编写MVC单元测试时遇空引用错误
在MVC项目中使用AutoFixture和NSubstitute编写单元测试时,Update和Delete方法触发“对象引用未设置到对象实例”错误,错误发生在业务代码的var parameter = await repository.GetFirstOrDefaultAsync(predicate: q => q.Id == parameterDto.Id);行,推测可能是包配置或测试依赖模拟存在遗漏。
业务代码
public async Task<ServiceResponse<ParameterDto>> Update(ParameterDto parameterDto) { try { if (parameterDto == null) { return serviceResponseHelper.SetError<ParameterDto>(null, localize["InvalidModel"], StatusCodes.Status400BadRequest, true); } var parameter = await repository.GetFirstOrDefaultAsync(predicate: q => q.Id == parameterDto.Id); if (parameter != null) { parameter.CdParam = parameterDto.CdParam; parameter.CdPlant = parameterDto.CdPlant; parameter.ChDescr = parameterDto.ChDescr; parameter.ChParam = parameterDto.ChParam; parameter.NoOrder = parameterDto.NoOrder; parameter.SwActive = parameterDto.SwActive; parameter.SwAllowDel = parameterDto.SwAllowDel; parameter.SwAllowNew = parameterDto.SwAllowNew; parameter.SwAllowUpd = parameterDto.SwAllowUpd; parameter.SwParent = parameterDto.SwParent; await repository.UpdateAsync(parameter).ConfigureAwait(false); var updatedParameter = await repository.GetFirstOrDefaultAsync(predicate: p => p.Id == parameterDto.Id); var dto = mapper.Map<Domain.Definition.Parameter, ParameterDto>(updatedParameter); return serviceResponseHelper.SetSuccess(dto); } return serviceResponseHelper.SetError<ParameterDto>(null, localize["ModelIsNotFound"], StatusCodes.Status400BadRequest, true); } catch (Exception ex) { return serviceResponseHelper.SetError<ParameterDto>(null, ex.InnerException?.Message ?? ex.Message, StatusCodes.Status500InternalServerError, true); } }
单元测试代码
[Fact] public async Task ParameterService_Update_ReturnsUpdatedParameterDto() { // Arrange var fixture = new Fixture(); var parameterDto = fixture.Create<ParameterDto>(); var parameter = fixture.Build<Domain.Definition.Parameter>() .With(p => p.Id, parameterDto.Id) // Set the same Id as the parameterDto for matching .Create(); var updatedParameter = fixture.Create<Domain.Definition.Parameter>(); var expectedResponse = new ServiceResponse<ParameterDto>(fixture.Create<ParameterDto>()); _repository.GetFirstOrDefaultAsync(Arg.Any<Expression<Func<Domain.Definition.Parameter, bool>>>(), null, null, default) .Returns(parameter); _repository.UpdateAsync(Arg.Any<Domain.Definition.Parameter>()) .Returns(Task.FromResult(updatedParameter)); _repository.GetFirstOrDefaultAsync(Arg.Any<Expression<Func<Domain.Definition.Parameter, bool>>>(), null, null, default) .Returns(updatedParameter); _mapper.Map<Domain.Definition.Parameter, ParameterDto>(Arg.Any<Domain.Definition.Parameter>()) .Returns(expectedResponse.Result); // Act var result = await _parameterService.Update(parameterDto); // Assert result.Should().BeEquivalentTo(expectedResponse); result.Result.Should().BeEquivalentTo(expectedResponse.Result); result.Should().NotBeNull(); result.IsSuccessful.Should().BeTrue(); }
问题分析与解决方案
核心问题1:Repository方法返回值被覆盖
NSubstitute中多次对同一个方法设置Returns时,最后一次配置会覆盖之前的。测试代码中先设置GetFirstOrDefaultAsync返回原parameter,随后又设置它返回updatedParameter,导致业务代码第一次调用该方法时,实际返回的是updatedParameter(而非预期的原parameter),若updatedParameter的Id与parameterDto.Id不匹配,会导致后续逻辑异常;更关键的是,如果_repository本身未被正确注入到_parameterService中,会直接触发空引用。
核心问题2:依赖未正确注入
确保_parameterService依赖的repository、serviceResponseHelper、localize、mapper均已通过NSubstitute创建模拟实例,并正确注入到服务类中,避免任何依赖为null。
修正步骤
调整Repository返回值配置
将两次GetFirstOrDefaultAsync的返回值设置合并为一次,用逗号分隔多个返回值,实现第一次调用返回原对象、第二次调用返回更新后对象:_repository.GetFirstOrDefaultAsync(Arg.Any<Expression<Func<Domain.Definition.Parameter, bool>>>(), null, null, default) .Returns(parameter, updatedParameter);验证依赖注入完整性
在测试类的初始化逻辑(如构造函数或[SetUp]方法)中,确保所有依赖都已初始化:private IParameterRepository _repository; private IMapper _mapper; private IServiceResponseHelper _serviceResponseHelper; private IStringLocalizer<ParameterService> _localize; private ParameterService _parameterService; [SetUp] public void Setup() { _repository = Substitute.For<IParameterRepository>(); _mapper = Substitute.For<IMapper>(); _serviceResponseHelper = Substitute.For<IServiceResponseHelper>(); _localize = Substitute.For<IStringLocalizer<ParameterService>>(); _parameterService = new ParameterService(_repository, _mapper, _serviceResponseHelper, _localize); }显式验证参数ID有效性
可添加断言确保parameterDto.Id不为默认值,避免查询逻辑无意义:parameterDto.Id.Should().NotBe(default);
内容的提问来源于stack exchange,提问作者sedya

