如何在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
相关产品推荐
相关产品推荐

