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

如何在C#中使用XUnit正确Mock MongoDB并编写工作单元模式下的单元测试

问题原因

你当前的测试写法存在两个核心问题:

  • 你实例化了真实的UnitOfWork类,但其内部的QuestionsRepository属性每次访问都会新建真实的QuestionsRepository实例,你既没有Mock该仓储的FindByIdAsync方法,也没有为Mock的QuestionsBankDBContext配置Database、IMongoCollection等关联依赖,导致调用时触发空引用异常。
  • 针对控制器层的单元测试不需要层层Mock MongoDB上下文,直接Mock上游依赖IUnitOfWork和IQuestionsRepository即可,可大幅降低Mock复杂度。
正确的单元测试实现

1. 依赖包准备

确保你已安装以下NuGet包:

  • Moq
  • xunit
  • xunit.runner.visualstudio

2. 单元测试代码示例

using Moq;
using Xunit;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using QuestionBank.API.Controllers;
using QuestionBank.API.Services;
using QuestionBank.API.Repositories;
using QuestionBank.API.Models;
using Microsoft.Extensions.Localization;

namespace QuestionBank.UnitTests;

public class QuestionsControllerTests
{
    // 声明Mock依赖
    private readonly Mock<IUnitOfWork> _mockUnitOfWork;
    private readonly Mock<IQuestionsRepository> _mockQuestionsRepository;
    private readonly Mock<IStringLocalizer<SharedResource>> _mockStringLocalizer;
    private readonly Mock<IQuestionBankIntergrationEventService> _mockIntegrationEventService;
    private readonly QuestionsController _questionsController;
    private readonly QuestionsService _questionsService;
    private readonly CampaignQuestionsService _campaignQuestionsService;
    private readonly TokenService _tokenService;

    public QuestionsControllerTests()
    {
        // 初始化Mock对象
        _mockUnitOfWork = new Mock<IUnitOfWork>();
        _mockQuestionsRepository = new Mock<IQuestionsRepository>();
        _mockStringLocalizer = new Mock<IStringLocalizer<SharedResource>>();
        _mockIntegrationEventService = new Mock<IQuestionBankIntergrationEventService>();
        
        // 配置UnitOfWork返回Mock的QuestionsRepository
        _mockUnitOfWork.Setup(u => u.QuestionsRepository).Returns(_mockQuestionsRepository.Object);

        // 初始化服务和控制器
        _questionsService = new QuestionsService(_mockUnitOfWork.Object);
        _campaignQuestionsService = new CampaignQuestionsService(_mockUnitOfWork.Object);
        _tokenService = new TokenService();
        
        _questionsController = new QuestionsController(
            _questionsService, 
            _campaignQuestionsService, 
            _mockStringLocalizer.Object, 
            _tokenService, 
            _mockIntegrationEventService.Object);
    }

    [Theory]
    [InlineData("61879e54e86be1fa5e41831f", true)] // 存在的ID返回200
    [InlineData("61879e54e86be1fa5e41831e", false)] // 不存在的ID返回404
    [InlineData("", false)] // 空ID返回404
    public async Task GetQuestionById_ReturnsExpectedResult(string questionId, bool isExist)
    {
        // 构造测试数据
        var mockQuestion = new Questions
        {
            Id = questionId,
            Content = "测试问题"
        };

        // 配置Mock方法返回值
        if (!string.IsNullOrEmpty(questionId))
        {
            _mockQuestionsRepository.Setup(r => r.FindByIdAsync(questionId))
                .ReturnsAsync(isExist ? mockQuestion : null);
        }

        // 配置多语言返回值
        _mockStringLocalizer.Setup(l => l[Constants.ErrorCodeString]).Returns("{0}: {1}");
        _mockStringLocalizer.Setup(l => l[Helper.ToEnumString(ErroCodes.SpecifiedItemNotFound)]).Returns("指定资源未找到");
        _mockStringLocalizer.Setup(l => l[Helper.ToEnumString(ErroCodes.InternalServerError)]).Returns("服务器内部错误");

        // 执行测试
        var actionResult = await _questionsController.GetQuestionById(questionId);
        var result = actionResult as ObjectResult;

        // 断言结果
        Assert.NotNull(result);
        if (string.IsNullOrEmpty(questionId) || !isExist)
        {
            Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode);
            Assert.Contains("ErrorCode", result.Value.ToString());
        }
        else
        {
            Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
            Assert.IsType<Questions>(result.Value);
            Assert.Equal(questionId, ((Questions)result.Value).Id);
        }
    }

    [Fact]
    public async Task GetQuestionById_ThrowException_Returns500()
    {
        // 配置仓储方法抛出异常
        const string testId = "test_id";
        _mockQuestionsRepository.Setup(r => r.FindByIdAsync(testId))
            .ThrowsAsync(new Exception("模拟数据库异常"));
        
        // 配置多语言返回值
        _mockStringLocalizer.Setup(l => l[Constants.ErrorCodeString]).Returns("{0}: {1}");
        _mockStringLocalizer.Setup(l => l[Helper.ToEnumString(ErroCodes.InternalServerError)]).Returns("服务器内部错误");

        // 执行测试
        var actionResult = await _questionsController.GetQuestionById(testId);
        var result = actionResult as ObjectResult;

        // 断言
        Assert.NotNull(result);
        Assert.Equal(StatusCodes.Status500InternalServerError, result.StatusCode);
        Assert.Contains(((int)ErroCodes.InternalServerError).ToString(), result.Value.ToString());
    }
}
优化建议(可选)

你当前的UnitOfWork类设计存在冗余:每次访问仓储属性都会新建实例,不仅有性能损耗,也不利于依赖注入和测试,建议修改为单例初始化:

public IQuestionsRepository QuestionsRepository
{
    get
    {
        return _questionsRepository ??= new QuestionsRepository(_context, this, _logger);
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 00:45:02