如何在NUnit中使用Moq正确填充Mock<DbSet>以测试仓储删除方法
我是NUnit测试新手,尝试测试仓储中的DeleteQuestion方法,但填充Mock<DbSet>测试实体时不断抛出异常。
待测试的DeleteQuestion方法代码
public async Task DeleteQuestion(Question questionToRemove) { if (questionToRemove is not null) { var questionsAnswers = await _context.Answers.Where(a => a.Question == questionToRemove).ToListAsync(); foreach (Answer a in questionsAnswers) { _context.Answers.Remove(a); await _context.SaveChangesAsync(); } _context.Questions.Remove(questionToRemove); await _context.SaveChangesAsync(); var questions = await _context.Questions.Where(q => q.Quiz == questionToRemove.Quiz).ToListAsync(); int order = 1; foreach (Question question in questions) { question.QuestionOrderId = order; await _context.SaveChangesAsync(); order++; } } }
我的测试代码
public async Task DeleteQuestionFunction() { //Arrange Question testQuestion = new Question { QuestionId = 1, QuestionText = "Test" }; Answer testAnswer = new Answer { Question = testQuestion, AnswerId = 1, AnswerText = "TestAnswer" }; _appDBContextMock.SetupAdd(x => x.Questions.Add(testQuestion)); _appDBContextMock.SetupAdd(x => x.Answers.Add(testAnswer)); //Act var questionListcomponent = new QuestionListComponent(); questionListcomponent.DeleteQuestion(testQuestion); var testresult = await _questionRepositoryMock.Object.GetQuestionById(testQuestion.QuestionId); //Assert Assert.That(testresult, Is.Null); }
遇到的异常
- 使用Setup方法时抛出:
System.NotSupportedException : Unsupported expression: x =>
x.Questions Non-overridable members (here: AppDbContext.get_Questions)
may not be used in setup / verification expressions.
- 使用SetupAdd方法时抛出:
System.ArgumentException : Can not instantiate proxy of class:
TietoQuiz.Models.DbContexts.AppDbContext. Could not find a
parameterless constructor. (Parameter 'constructorArguments')
我认为问题在于不知道如何正确设置Mock<DbSet>,搜索到的方案杂乱且褒贬不一,作为新手十分困惑,希望获得帮助。
核心问题分析
- 非可重写成员报错:直接Mock
AppDbContext的Questions/Answers属性时,EF Core的DbSet<T>默认是类属性,并非虚成员,Moq无法拦截非虚成员的调用,因此抛出异常。 - 无参构造函数报错:
AppDbContext没有无参构造函数,Moq无法直接创建它的代理对象,导致SetupAdd调用失败。
正确设置Mock的步骤
1. 准备测试数据并封装成可查询的Mock DbSet
先将测试实体转为IQueryable,再用Moq创建Mock<DbSet<T>>,并配置它支持LINQ查询和异步操作:
// 创建测试数据列表 var testQuestions = new List<Question> { new Question { QuestionId = 1, QuestionText = "Test", Quiz = new Quiz() } // 确保Quiz不为null,方法中会用到 }; var testAnswers = new List<Answer> { new Answer { Question = testQuestions[0], AnswerId = 1, AnswerText = "TestAnswer" } }; // 转为IQueryable var questionsQueryable = testQuestions.AsQueryable(); var answersQueryable = testAnswers.AsQueryable(); // 创建Mock DbSet<Question>并配置IQueryable支持 var mockQuestionsDbSet = new Mock<DbSet<Question>>(); mockQuestionsDbSet.As<IQueryable<Question>>().Setup(m => m.Provider).Returns(questionsQueryable.Provider); mockQuestionsDbSet.As<IQueryable<Question>>().Setup(m => m.Expression).Returns(questionsQueryable.Expression); mockQuestionsDbSet.As<IQueryable<Question>>().Setup(m => m.ElementType).Returns(questionsQueryable.ElementType); mockQuestionsDbSet.As<IQueryable<Question>>().Setup(m => m.GetEnumerator()).Returns(questionsQueryable.GetEnumerator()); // 配置异步ToListAsync支持 mockQuestionsDbSet.As<IAsyncEnumerable<Question>>() .Setup(m => m.GetAsyncEnumerator(It.IsAny<CancellationToken>())) .Returns(new AsyncEnumerator<Question>(questionsQueryable.GetEnumerator())); // 同理创建并配置Mock DbSet<Answer> var mockAnswersDbSet = new Mock<DbSet<Answer>>(); mockAnswersDbSet.As<IQueryable<Answer>>().Setup(m => m.Provider).Returns(answersQueryable.Provider); mockAnswersDbSet.As<IQueryable<Answer>>().Setup(m => m.Expression).Returns(answersQueryable.Expression); mockAnswersDbSet.As<IQueryable<Answer>>().Setup(m => m.ElementType).Returns(answersQueryable.ElementType); mockAnswersDbSet.As<IQueryable<Answer>>().Setup(m => m.GetEnumerator()).Returns(answersQueryable.GetEnumerator()); mockAnswersDbSet.As<IAsyncEnumerable<Answer>>() .Setup(m => m.GetAsyncEnumerator(It.IsAny<CancellationToken>())) .Returns(new AsyncEnumerator<Answer>(answersQueryable.GetEnumerator()));
需要添加以下辅助类支持异步枚举:
public class AsyncEnumerator<T> : IAsyncEnumerator<T> { private readonly IEnumerator<T> _enumerator; public AsyncEnumerator(IEnumerator<T> enumerator) { _enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator)); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(_enumerator.MoveNext()); public T Current => _enumerator.Current; }
2. 正确Mock DbContext
由于AppDbContext没有无参构造函数,Mock时需要传入它所需的构造参数(比如DbContextOptions<AppDbContext>):
// 创建空的DbContextOptions var options = new DbContextOptionsBuilder<AppDbContext>().Options; var _appDBContextMock = new Mock<AppDbContext>(options); // 配置Context的属性返回Mock的DbSet _appDBContextMock.Setup(c => c.Questions).Returns(mockQuestionsDbSet.Object); _appDBContextMock.Setup(c => c.Answers).Returns(mockAnswersDbSet.Object); // Mock SaveChangesAsync,方法中多次调用该方法 _appDBContextMock.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(1); // 返回1表示保存成功
3. 修正测试代码
调整测试方法,确保依赖注入正确,异步方法调用加await:
public async Task DeleteQuestionFunction() { //Arrange // 将上述测试数据、Mock DbSet、DbContext的代码放置此处 // 注入Mock的DbContext到组件中(替换为实际构造注入方式) var questionListcomponent = new QuestionListComponent(_appDBContextMock.Object); //Act await questionListcomponent.DeleteQuestion(testQuestions[0]); // 异步方法必须加await //Assert // 验证Questions的Remove方法被调用一次 mockQuestionsDbSet.Verify(m => m.Remove(testQuestions[0]), Times.Once); // 验证Answers的Remove方法被调用一次 mockAnswersDbSet.Verify(m => m.Remove(testAnswers[0]), Times.Once); // 验证SaveChangesAsync的调用次数(删除Answer1次+删除Question1次+更新Question1次,共3次) _appDBContextMock.Verify(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Exactly(3)); // 验证QuestionOrderId被正确更新 Assert.That(testQuestions[0].QuestionOrderId, Is.EqualTo(1)); }
额外建议
- 将
DbContext的DbSet属性改为虚属性,简化Moq拦截:
public class AppDbContext : DbContext { public virtual DbSet<Question> Questions { get; set; } public virtual DbSet<Answer> Answers { get; set; } // ...其他代码 }
- 可以使用
EntityFrameworkCore.Testing.Moq第三方库,无需手动编写异步枚举辅助类,快速完成DbSet的Mock。
内容的提问来源于stack exchange,提问作者Yumaisch

