ASP.NET MVC EmployeePage返回员工列表页单元测试编写求助
EmployeePage 动作方法单元测试方案
核心测试校验点
针对给出的控制器逻辑,单元测试需要覆盖以下核心规则,确保代码行为符合预期:
- 方法返回结果类型为
ViewResult,不会出现跳转、报错页等不符合预期的结果 - 传入视图的模型类型为
List<EmployeeView> - 员工实体到视图模型的字段映射完全准确:Id、Name、Address、DepartmentId字段值和源数据一致,DepartmentName能正确读取关联部门的名称
- 员工表无数据时,方法正常返回空列表,不会抛出空引用等异常
方案1:最小代码侵入(几乎不用改生产代码,基于EF内存数据库实现)
如果不想大改现有业务代码,直接用EF对应的内存数据库提供程序构造测试用的数据上下文,完全隔离真实生产数据库,不会改动线上业务数据。
这里只需要给现有控制器加极小量的调整,完全不影响线上运行逻辑:给控制器加一个接收
MVCTutorialsEntities参数的构造函数,把Action方法里直接new MVCTutorialsEntities()的代码替换成调用类内初始化的_db字段即可。无参构造函数保留,平时线上运行还是默认连真实数据库,测试时把内存上下文传入即可。
调整后的控制器构造代码示例:
public class EmployeeController : Controller { private readonly MVCTutorialsEntities _db; // 原有默认构造函数保留,线上逻辑不变 public EmployeeController() { _db = new MVCTutorialsEntities(); } // 测试用构造函数 public EmployeeController(MVCTutorialsEntities db) { _db = db; } // EmployeePage方法里把原来的new MVCTutorialsEntities()删掉,直接用_db即可 public ActionResult EmployeePage() { List<Employee> employeeList = _db.Employees.ToList(); List<EmployeeView> empViewList = employeeList.Select(x => new EmployeeView { Id = x.Id, Name = x.Name, Address = x.Address, DepartmentId = x.DepartmentId, DepartmentName = x.Department.DepartmentName }).ToList(); return View(empViewList); } }
基于xUnit测试框架的测试代码如下(EF Core场景用官方InMemory包,EF6可替换为Effort.EF6内存提供程序,逻辑完全一致):
using Xunit; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using YourProject.Controllers; using YourProject.Models; using System; using System.Collections.Generic; using System.Linq; public class EmployeeControllerTests { // 构造带测试种子数据的内存上下文 private MVCTutorialsEntities GetTestDbContext() { var options = new DbContextOptionsBuilder<MVCTutorialsEntities>() // 每个测试用例用全新的库名,避免数据互相干扰 .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var db = new MVCTutorialsEntities(options); // 插入测试用的部门、员工数据 db.Departments.AddRange( new Department { Id = 1, DepartmentName = "研发部" }, new Department { Id = 2, DepartmentName = "人事部" } ); db.Employees.AddRange( new Employee { Id = 1, Name = "张三", Address = "北京", DepartmentId = 1 }, new Employee { Id = 2, Name = "李四", Address = "上海", DepartmentId = 2 } ); db.SaveChanges(); return db; } [Fact] public void EmployeePage_Should_Return_Correct_Mapped_View_Model() { // 初始化测试环境 using var db = GetTestDbContext(); var controller = new EmployeeController(db); // 执行被测方法 var result = controller.EmployeePage(); // 校验结果 // 1. 确认返回的是视图结果 var viewResult = Assert.IsType<ViewResult>(result); // 2. 确认传入视图的模型类型正确 var empList = Assert.IsAssignableFrom<List<EmployeeView>>(viewResult.Model); // 3. 确认返回数据条数正确 Assert.Equal(2, empList.Count); // 4. 校验字段映射是否正确,重点校验关联的部门名称 var zhangsan = empList.First(e => e.Id == 1); Assert.Equal("张三", zhangsan.Name); Assert.Equal("北京", zhangsan.Address); Assert.Equal(1, zhangsan.DepartmentId); Assert.Equal("研发部", zhangsan.DepartmentName); var lisi = empList.First(e => e.Id == 2); Assert.Equal("李四", lisi.Name); Assert.Equal("上海", lisi.Address); Assert.Equal(2, lisi.DepartmentId); Assert.Equal("人事部", lisi.DepartmentName); } [Fact] public void EmployeePage_Should_Return_Empty_List_When_No_Data() { // 构造无员工数据的测试上下文 var options = new DbContextOptionsBuilder<MVCTutorialsEntities>() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; using var db = new MVCTutorialsEntities(options); db.Departments.Add(new Department { Id = 1, DepartmentName = "研发部" }); db.SaveChanges(); var controller = new EmployeeController(db); // 执行方法 var result = controller.EmployeePage(); // 校验结果 var viewResult = Assert.IsType<ViewResult>(result); var empList = Assert.IsAssignableFrom<List<EmployeeView>>(viewResult.Model); Assert.Empty(empList); } }
方案2:完全解耦的高速单元测试(基于Mock框架,无数据库依赖)
如果追求更快的测试执行速度(适合测试用例量很大的场景),可以把数据访问逻辑抽成仓储接口,用Moq框架直接模拟数据返回,完全不依赖EF和数据库,测试运行速度可以提升一个量级。
该方案需要把直接操作EF上下文的逻辑抽离到仓储层,控制器依赖仓储抽象而非具体EF实现,符合依赖倒置原则,后续换数据库实现也不用改控制器代码。
核心测试代码示例:
using Moq; using Xunit; using Microsoft.AspNetCore.Mvc; using YourProject.Controllers; using YourProject.Models; using YourProject.Repositories; using System.Collections.Generic; using System.Linq; public class EmployeeControllerMockTests { [Fact] public void EmployeePage_Should_Map_Data_Correctly_With_Mock_Repo() { // 初始化模拟仓储 var mockEmployeeRepo = new Mock<IEmployeeRepository>(); // 配置模拟仓储的返回数据 var testData = new List<Employee> { new Employee { Id = 1, Name = "王五", Address = "广州", DepartmentId = 3, Department = new Department { DepartmentName = "财务部" } } }; mockEmployeeRepo.Setup(repo => repo.GetAllWithDepartment()).Returns(testData); var controller = new EmployeeController(mockEmployeeRepo.Object); // 执行被测方法 var result = controller.EmployeePage(); // 校验结果 var viewResult = Assert.IsType<ViewResult>(result); var empList = Assert.IsAssignableFrom<List<EmployeeView>>(viewResult.Model); var emp = empList.Single(); Assert.Equal(1, emp.Id); Assert.Equal("王五", emp.Name); Assert.Equal("广州", emp.Address); Assert.Equal(3, emp.DepartmentId); Assert.Equal("财务部", emp.DepartmentName); // 确认仓储方法确实被调用了一次,没有多余的数据请求 mockEmployeeRepo.Verify(repo => repo.GetAllWithDepartment(), Times.Once); } }
测试注意事项
- 每个测试用例必须用独立的上下文,不要共享数据库实例,避免前一个用例插入的测试数据影响后一个用例的结果
- 一定要校验
DepartmentName的映射结果,关联表数据加载失败是这类代码最常见的空引用bug来源 - 如果需要测试视图页面的表格渲染是否正确,可以补充Razor视图测试,校验渲染后的HTML包含对应员工的字段值即可,核心业务逻辑通过控制器动作测试就能覆盖大部分场景
- 要是项目用的是ASP.NET MVC (.NET Framework) 版本的EF6,把InMemory提供程序换成Effort.EF6即可,测试逻辑不需要改动
内容的提问来源于stack exchange,提问作者Deven Thakur
相关产品推荐
相关产品推荐

