使用xUnit测试EF中Attach方法时触发NullReferenceException问题排查
问题说明
为更新服务编写xUnit测试时触发空引用异常,报错行指向_dbContext.Entry(user).Property(x => x.Name).IsModified = true;,不清楚如何正确Mock相关对象解决该问题。
Update方法代码
public bool Update(string Id, CreateUpdateUserDto InP) { var user = new UserDetails { Id = Id, Name = InP.Name }; _dbContext.userDetails.Attach(user); _dbContext.Entry(user).Property(x => x.Name).IsModified = true; return _dbContext.SaveChanges() > 0; }
测试用例代码
private readonly Mock<UserDbContext> _mockDB; private UserRepository _repo; public UserRepositoryTests() { var data = new List<UserDetails> { new UserDetails{Id="GuId1",Name="Test1"}, new UserDetails{Id="GuId2",Name="Test2"}, new UserDetails{Id="GuId3",Name="Test3"} }.AsQueryable(); var mockSet = new Mock<DbSet<UserDetails>>(); mockSet.As<IQueryable<UserDetails>>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As<IQueryable<UserDetails>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<UserDetails>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<UserDetails>>().Setup(x => x.GetEnumerator()).Returns(() => data.GetEnumerator()); _mockDB = new Mock<UserDbContext>(); _mockDB.Setup(y=>y.userDetails).Returns(mockSet.Object); _repo = new UserRepository(_mockDB.Object); } [Fact] public void UpdateSuccess() { var InP = new CreateUpdateUserDto() { Name = "Test" }; var result = _repo.Update("GuId1", InP); Assert.IsType<bool>(result); Assert.True(result); }
报错信息
System.NullReferenceException: Object reference not set to an instance of an object.
Stack Trace:
UserRepository.Update(String Id, CreateUpdateUserDto InP) line 35
UserRepositoryTests.UpdateSuccess() line 71
解决方案
空引用异常是因为仅Mock了DbSet<UserDetails>,但未MockDbContext.Entry<TEntity>方法的返回值,调用_dbContext.Entry(user)时返回null,导致后续访问Property抛出异常。需补充以下Mock配置:
1. 完善测试初始化逻辑
在测试类的构造函数中,_mockDB.Setup(y=>y.userDetails).Returns(mockSet.Object);之后添加以下代码:
// Mock DbContext.Entry方法,返回模拟的EntityEntry<UserDetails>实例 var mockEntityEntry = new Mock<EntityEntry<UserDetails>>(); // Mock EntityEntry.Property方法,返回模拟的PropertyEntry实例 var mockPropertyEntry = new Mock<PropertyEntry>(); mockEntityEntry.Setup(e => e.Property(It.IsAny<Expression<Func<UserDetails, object>>>())).Returns(mockPropertyEntry.Object); // 配置DbContext.Entry返回上述模拟实例 _mockDB.Setup(c => c.Entry(It.IsAny<UserDetails>())).Returns(mockEntityEntry.Object); // Mock SaveChanges方法,返回1表示更新操作成功 _mockDB.Setup(c => c.SaveChanges()).Returns(1);
2. 验证修改效果
修改后,_dbContext.Entry(user)会返回模拟的EntityEntry实例,调用Property可拿到模拟的PropertyEntry,设置IsModified不会触发空引用;同时SaveChanges返回1,测试断言会顺利通过。
若需验证IsModified是否被正确设置,可在测试方法末尾添加验证代码:
mockPropertyEntry.VerifySet(p => p.IsModified = true, Times.Once);
完整修改后的测试初始化代码
public UserRepositoryTests() { var data = new List<UserDetails> { new UserDetails{Id="GuId1",Name="Test1"}, new UserDetails{Id="GuId2",Name="Test2"}, new UserDetails{Id="GuId3",Name="Test3"} }.AsQueryable(); var mockSet = new Mock<DbSet<UserDetails>>(); mockSet.As<IQueryable<UserDetails>>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As<IQueryable<UserDetails>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<UserDetails>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<UserDetails>>().Setup(x => x.GetEnumerator()).Returns(() => data.GetEnumerator()); _mockDB = new Mock<UserDbContext>(); _mockDB.Setup(y=>y.userDetails).Returns(mockSet.Object); // 新增的Mock配置 var mockEntityEntry = new Mock<EntityEntry<UserDetails>>(); var mockPropertyEntry = new Mock<PropertyEntry>(); mockEntityEntry.Setup(e => e.Property(It.IsAny<Expression<Func<UserDetails, object>>>())).Returns(mockPropertyEntry.Object); _mockDB.Setup(c => c.Entry(It.IsAny<UserDetails>())).Returns(mockEntityEntry.Object); _mockDB.Setup(c => c.SaveChanges()).Returns(1); _repo = new UserRepository(_mockDB.Object); }
补充说明
Update方法中直接创建UserDetails实例并Attach的逻辑在真实DbContext中可行,但Mock场景下需确保所有被调用的DbContext成员都被正确模拟。后续若需修改其他属性,只需扩展mockEntityEntry的Setup配置即可。
内容的提问来源于stack exchange,提问作者Upender Reddy

