EF Core DbContext包装类单元测试空引用异常解决方法咨询
问题描述
我封装了EF Core DbContext的DatabaseConnector类,在Rest API控制器中正常工作,但编写单元测试时触发Object reference not set to an instance of an object(空引用异常)。相关代码如下:
DatabaseConnector类代码
using System.Diagnostics; using Microsoft.EntityFrameworkCore; using DatabaseProvider.Interfaces; namespace DatabaseProvider.Classes { public class DatabaseConnector(DbContext context) : IDatabaseConnector { private readonly DbContext _context = context; public async Task<List<T>> GetAsync<T>() where T : class { try { return await _context.Set<T>().AsQueryable().ToListAsync(); } catch (Exception exc) { Debug.Write(exc.Message); throw; } } public async Task<T?> GetByIdAsync<T>(int id) where T : class { try { return await _context.FindAsync<T>(id); } catch (Exception exc) { Debug.Write(exc.Message); throw; } } public async Task<bool> CreateAsync<T>(T entity) { try { if (entity != null) { _context.Add(entity); await _context.SaveChangesAsync(); return true; } return false; } catch (Exception exc) { Debug.Write(exc.Message); return false; } } public async Task<bool> UpdateAsync<T>(T entity) { try { if (entity != null) { _context.Update(entity); await _context.SaveChangesAsync(); return true; } return false; } catch (Exception exc) { Debug.Write(exc.Message); return false; } } public async Task<bool> DeleteAsync<T>(T entity) { try { if (entity != null) { _context.Remove(entity); await _context.SaveChangesAsync(); return true; } return false; } catch (Exception exc) { Debug.Write(exc.Message); return false; } } } }
单元测试类代码
using Moq; using DatabaseProvider.Interfaces; using DatabaseProvider.Models; using DatabaseProvider.Classes; namespace NUnitTests.DatabaseProvider { public class DatabaseConnectorTests { private Mock<TestDbContext> _context; private IDatabaseConnector _connector; [SetUp] public void Setup() { _context = new Mock<TestDbContext>(); var abouts = DatabaseDataFake.GetFakeAboutList(); _context.Setup(x => x.Abouts).Returns(MockCreater.GetQueryableMockDbSet(abouts)); _connector = new DatabaseConnector(_context.Object); } [Test] public async Task Get_About_List() { var result = await _connector.GetAsync<About>(); Assert.That(result, Is.Not.Null); Assert.That(result, Has.Count.EqualTo(2)); } } }
MockDbSet创建类代码
using Moq; using Microsoft.EntityFrameworkCore; namespace NUnitTests { public static class MockCreater { public static DbSet<T> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class { var queryable = sourceList.AsQueryable(); var dbSet = new Mock<DbSet<T>>(); dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider); dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression); dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType); dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator); dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>(sourceList.Add); return dbSet.Object; } } }
解决方案
问题根因
单元测试中你Mock了TestDbContext.Abouts属性,但DatabaseConnector.GetAsync<T>方法调用的是DbContext.Set<T>()来获取实体集合,而非直接访问上下文的实体属性,导致Set<T>()返回null,触发空引用异常。此外,当前的MockDbSet未支持EF Core的异步查询方法(如ToListAsync()),即使解决了空引用,异步调用仍可能报错。
方法一:修正Moq配置
1. 调整测试类的Setup逻辑
将原来MockAbouts属性的代码,改为MockSet<About>()方法:
[SetUp] public void Setup() { _context = new Mock<TestDbContext>(); var abouts = DatabaseDataFake.GetFakeAboutList(); var aboutsDbSet = MockCreater.GetQueryableMockDbSet(abouts); // 改为Mock Set<T>方法,而非具体的DbSet属性 _context.Setup(x => x.Set<About>()).Returns(aboutsDbSet); _connector = new DatabaseConnector(_context.Object); }
2. 增强MockDbSet以支持异步查询
当前的MockDbSet仅实现了同步IQueryable接口,无法支持ToListAsync()等异步方法,需要补充异步相关的Mock:
更新MockCreater类,添加对IAsyncEnumerable和异步Provider的支持:
using Moq; using Microsoft.EntityFrameworkCore; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using System.Linq.Async; // 需要安装System.Linq.Async NuGet包 namespace NUnitTests { public static class MockCreater { public static DbSet<T> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class { var queryable = sourceList.AsQueryable(); var dbSet = new Mock<DbSet<T>>(); // 同步IQueryable配置 dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new AsyncQueryProvider<T>(queryable.Provider)); dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression); dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType); dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator); // 异步IAsyncEnumerable配置 dbSet.As<IAsyncEnumerable<T>>() .Setup(m => m.GetAsyncEnumerator(It.IsAny<CancellationToken>())) .Returns(new AsyncEnumerator<T>(queryable.GetEnumerator())); // Add方法配置 dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>(sourceList.Add); return dbSet.Object; } // 自定义异步查询提供器 private class AsyncQueryProvider<TEntity> : IAsyncQueryProvider { private readonly IQueryProvider _inner; public AsyncQueryProvider(IQueryProvider inner) { _inner = inner; } public IQueryable CreateQuery(Expression expression) { return new AsyncEnumerable<TEntity>(expression); } public IQueryable<TElement> CreateQuery<TElement>(Expression expression) { return new AsyncEnumerable<TElement>(expression); } public object Execute(Expression expression) { return _inner.Execute(expression); } public TResult Execute<TResult>(Expression expression) { return _inner.Execute<TResult>(expression); } public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken) { return Task.FromResult(Execute(expression)); } public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) { return Task.FromResult(Execute<TResult>(expression)); } } // 自定义异步枚举器 private class AsyncEnumerable<TEntity> : EnumerableQuery<TEntity>, IAsyncEnumerable<TEntity>, IQueryable<TEntity> { public AsyncEnumerable(Expression expression) : base(expression) { } public IAsyncEnumerator<TEntity> GetAsyncEnumerator(CancellationToken cancellationToken = default) { return new AsyncEnumerator<TEntity>(this.AsEnumerable().GetEnumerator()); } IQueryProvider IQueryable.Provider => new AsyncQueryProvider<TEntity>(this.Provider); } // 自定义异步枚举器实现 private class AsyncEnumerator<TEntity> : IAsyncEnumerator<TEntity> { private readonly IEnumerator<TEntity> _inner; public AsyncEnumerator(IEnumerator<TEntity> inner) { _inner = inner; } public ValueTask DisposeAsync() { _inner.Dispose(); return ValueTask.CompletedTask; } public ValueTask<bool> MoveNextAsync() { return ValueTask.FromResult(_inner.MoveNext()); } public TEntity Current => _inner.Current; } } }
方法二:改用EF Core内存数据库(更简单可靠)
手动Mock DbContext和DbSet容易遗漏方法,推荐使用EF Core的In-Memory数据库进行单元测试,无需复杂Mock:
1. 安装In-Memory数据库NuGet包
Install-Package Microsoft.EntityFrameworkCore.InMemory
2. 修改测试类的Setup逻辑
[SetUp] public void Setup() { // 创建内存数据库上下文 var options = new DbContextOptionsBuilder<TestDbContext>() .UseInMemoryDatabase(databaseName: "TestDatabase") .Options; var context = new TestDbContext(options); // 初始化测试数据 var abouts = DatabaseDataFake.GetFakeAboutList(); context.Abouts.AddRange(abouts); context.SaveChanges(); _connector = new DatabaseConnector(context); }
这种方式完全模拟真实的EF Core行为,无需手动Mock任何方法,异步查询、增删改查都能正常工作,测试结果更可靠。
内容的提问来源于stack exchange,提问作者Kanasi
相关产品推荐
相关产品推荐

