使用xUnit与Moq做单元测试时,TeacherController返回空Model集合排查
TeacherController Index方法返回Model集合为空的原因与解决办法
问题场景
调试TeacherController的Index方法时,单元测试中发现返回的Model集合为空,使用Moq和xUnit进行测试。
相关代码
TeacherController
public class TeacherController : Controller { private IClassScheduleUnitOfWork _classScheduleUnitOfWork { get; set; } public TeacherController(IClassScheduleUnitOfWork classScheduleUnitOfWork) { _classScheduleUnitOfWork = classScheduleUnitOfWork; } public ViewResult Index() { var options = new QueryOptions<Teacher> { OrderBy = t => t.LastName }; IEnumerable<Teacher> list = _classScheduleUnitOfWork.teachers.List(options); return View("Index", list); } }
单元测试代码
public class TeacherControllerTests { private readonly Mock<IClassScheduleUnitOfWork> _mock; public TeacherControllerTests() { _mock = new Mock<IClassScheduleUnitOfWork>(); } [Fact] public void IndexActionMethod_ReturnsAViewResult() { var options = new QueryOptions<Teacher> { OrderBy = t => t.LastName }; IEnumerable<Teacher> teachersList = GetAllTeachers(); _mock.Setup(x =>x.teachers.List(options)) .Returns(teachersList); var controller = new TeacherController(_mock.Object); //act var result = controller.Index(); var actualData = (IEnumerable<Teacher>?)result?.ViewData.Model; Assert.Equal(5, actualData?.Count()); } private IEnumerable<Teacher> GetAllTeachers() { IEnumerable<Teacher> teachers = new List<Teacher>() { new Teacher { TeacherId = 1, FirstName = "Anne", LastName = "Sullivan" }, new Teacher { TeacherId = 2, FirstName = "Maria", LastName = "Montessori" }, new Teacher { TeacherId = 3, FirstName = "Martin Luther", LastName = "King" }, new Teacher { TeacherId = 4, FirstName = "", LastName = "Aristotle" }, new Teacher { TeacherId = 5, FirstName = "Jaime", LastName = "Escalante" } }; teachers = teachers.OrderBy(t => t.LastName).ToList(); return teachers; } }
IClassScheduleUnitOfWork定义
public interface IClassScheduleUnitOfWork { public IRepository<Teacher> teachers { get; set; } public IRepository<Class> classes { get; set; } public IRepository<Day> days { get; set; } public void Save(); }
IRepository代码
public interface IRepository<T> where T : class { IEnumerable<T> List(QueryOptions<T> options); T? Get(int id); T? Get(QueryOptions<T> queryOptions); void Insert(T entity); void Update(T entity); void Delete(T entity); void Save(); }
问题原因
核心问题在于Moq的参数匹配逻辑:
测试代码中,Setup时指定的是自己创建的options实例,但控制器的Index方法会新建一个独立的QueryOptionsList方法的默认值(空集合),导致View的Model为空。
解决办法
方法1:匹配任意QueryOptions实例(最简单)
修改测试中的Setup代码,使用It.IsAny<QueryOptions<Teacher>>()匹配任意类型的参数,确保控制器传入的新实例能触发Setup:
_mock.Setup(x => x.teachers.List(It.IsAny<QueryOptions<Teacher>>())) .Returns(teachersList);
方法2:匹配符合特定条件的QueryOptions(更精准)
如果需要验证控制器传入的QueryOptions是否符合预期(比如OrderBy确实是按LastName排序),可以使用It.Is<QueryOptions<Teacher>>()进行条件匹配:
_mock.Setup(x => x.teachers.List(It.Is<QueryOptions<Teacher>>(opt => // 验证OrderBy表达式是否正确 opt.OrderBy != null && opt.OrderBy.Compile()(new Teacher { LastName = "Z" }).Equals("Z") && opt.OrderBy.Compile()(new Teacher { LastName = "A" }).Equals("A") ))) .Returns(teachersList);
方法3:让QueryOptions实现值相等匹配
修改QueryOptions<T>类,实现IEquatable<QueryOptions<T>>接口,重写相等比较逻辑,让Moq可以按值而非引用匹配参数。这种方式适合需要严格匹配参数所有属性的场景,但需要修改QueryOptions的源码。
修改后的测试代码示例(方法1)
[Fact] public void IndexActionMethod_ReturnsAViewResult() { IEnumerable<Teacher> teachersList = GetAllTeachers(); // 使用It.IsAny匹配任意QueryOptions实例 _mock.Setup(x => x.teachers.List(It.IsAny<QueryOptions<Teacher>>())) .Returns(teachersList); var controller = new TeacherController(_mock.Object); //act var result = controller.Index(); var actualData = (IEnumerable<Teacher>?)result?.ViewData.Model; Assert.Equal(5, actualData?.Count()); }
内容的提问来源于stack exchange,提问作者Affan Sheikh
相关产品推荐
相关产品推荐

