You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NUnit中使用Moq正确填充Mock<DbSet>以测试仓储删除方法

问题:NUnit测试仓储DeleteQuestion方法时Mock报错

我是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);
}

遇到的异常

  1. 使用Setup方法时抛出:

System.NotSupportedException : Unsupported expression: x =>
x.Questions Non-overridable members (here: AppDbContext.get_Questions)
may not be used in setup / verification expressions.

  1. 使用SetupAdd方法时抛出:

System.ArgumentException : Can not instantiate proxy of class:
TietoQuiz.Models.DbContexts.AppDbContext. Could not find a
parameterless constructor. (Parameter 'constructorArguments')

我认为问题在于不知道如何正确设置Mock<DbSet>,搜索到的方案杂乱且褒贬不一,作为新手十分困惑,希望获得帮助。


解决方案

核心问题分析

  1. 非可重写成员报错:直接MockAppDbContext的Questions/Answers属性时,EF Core的DbSet<T>默认是类属性,并非虚成员,Moq无法拦截非虚成员的调用,因此抛出异常。
  2. 无参构造函数报错: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));
}

额外建议

  1. 将DbContext的DbSet属性改为虚属性,简化Moq拦截:
public class AppDbContext : DbContext
{
    public virtual DbSet<Question> Questions { get; set; }
    public virtual DbSet<Answer> Answers { get; set; }
    // ...其他代码
}
  1. 可以使用EntityFrameworkCore.Testing.Moq第三方库,无需手动编写异步枚举辅助类,快速完成DbSet的Mock。

内容的提问来源于stack exchange,提问作者Yumaisch

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 08:40:46