基于Entity Framework的存储过程调用单元测试与代码覆盖率提升
为Entity Framework调用存储过程的代码添加单元测试并提升覆盖率
嘿,我来帮你搞定这个问题!要测试EF调用存储过程的代码,核心思路是Mock掉EF的核心组件,不用连真实数据库,同时验证你的代码是否正确调用了存储过程、处理返回结果。下面我结合你给的两个示例一步步讲:
先准备好工具
首先你需要用Moq这个Mock框架,它是.NET生态里最常用的。用NuGet安装:
Install-Package Moq
或者用.NET CLI:
dotnet add package Moq
测试示例1:通过DbSet.SqlQuery调用存储过程
你的第一个方法是PersonsFromSP(),依赖Persons这个DbSet<Person>。我们可以Mock这个DbSet,让它返回预设的测试数据,同时验证存储过程名称是否正确调用。
测试代码示例:
[TestClass] public class YourDbContextTests { [TestMethod] public void PersonsFromSP_ShouldReturnExpectedPersons() { // 1. 造点测试用的Person数据 var testPersons = new List<Person> { new Person { Id = 1, Name = "Alice" }, new Person { Id = 2, Name = "Bob" } }.AsQueryable(); // 2. Mock DbSet<Person>,让它能被当成IQueryable用 var mockPersonSet = new Mock<DbSet<Person>>(); mockPersonSet.As<IQueryable<Person>>().Setup(m => m.Provider).Returns(testPersons.Provider); mockPersonSet.As<IQueryable<Person>>().Setup(m => m.Expression).Returns(testPersons.Expression); mockPersonSet.As<IQueryable<Person>>().Setup(m => m.ElementType).Returns(testPersons.ElementType); mockPersonSet.As<IQueryable<Person>>().Setup(m => m.GetEnumerator()).Returns(testPersons.GetEnumerator()); // 3. 关键一步:Mock SqlQuery方法,确保调用的是正确的存储过程 mockPersonSet.Setup(m => m.SqlQuery("spGetPersons", It.IsAny<object[]>())) .Returns(testPersons); // 4. 把Mock的DbSet注入到Mock的DbContext里 var mockContext = new Mock<YourDbContext>(); mockContext.Setup(m => m.Persons).Returns(mockPersonSet.Object); // 5. 调用方法,检查结果对不对 var result = mockContext.Object.PersonsFromSP().ToList(); Assert.AreEqual(2, result.Count); Assert.AreEqual("Alice", result[0].Name); // 6. 验证存储过程确实被调用了一次,名称也没错 mockPersonSet.Verify(m => m.SqlQuery("spGetPersons", It.IsAny<object[]>()), Times.Once); } }
测试示例2:通过Database.SqlQuery调用存储过程
第二个方法AnimalsFromSP()用的是Database.SqlQuery<Animal>,这里有点麻烦:DatabaseFacade是密封类,Moq没法直接Mock它的扩展方法。解决办法是给你的DbContext加一层封装,把存储过程调用做成虚方法,这样就能轻松Mock了。
第一步:重构你的DbContext
先给YourDbContext加一个虚方法,封装存储过程调用:
public class YourDbContext : DbContext { // 你的其他DbSet和代码... public virtual IEnumerable<T> ExecuteStoredProcedure<T>(string storedProcedureName) { return Database.SqlQuery<T>(storedProcedureName).ToList(); } public virtual IEnumerable<Animal> AnimalsFromSP() { // 改成调用封装好的方法 return ExecuteStoredProcedure<Animal>("spGetAnimals"); } }
第二步:编写测试用例
现在就可以Mock这个封装后的虚方法了:
[TestMethod] public void AnimalsFromSP_ShouldReturnExpectedAnimals() { // 1. 准备测试动物数据 var testAnimals = new List<Animal> { new Animal { Id = 1, Species = "Dog" }, new Animal { Id = 2, Species = "Cat" } }.AsQueryable(); // 2. Mock DbContext,让ExecuteStoredProcedure返回我们的测试数据 var mockContext = new Mock<YourDbContext>(); mockContext.Setup(m => m.ExecuteStoredProcedure<Animal>("spGetAnimals")) .Returns(testAnimals); // 3. 调用方法并验证结果 var result = mockContext.Object.AnimalsFromSP().ToList(); Assert.AreEqual(2, result.Count); Assert.AreEqual("Dog", result[0].Species); // 4. 确认存储过程名称和调用次数都正确 mockContext.Verify(m => m.ExecuteStoredProcedure<Animal>("spGetAnimals"), Times.Once); }
提升代码覆盖率的小技巧
要拉满覆盖率,得覆盖所有场景:
- 分支全覆盖:如果你的业务逻辑里有if/else、switch这些条件判断,每个分支都要写测试用例,比如空结果、单条结果、多条结果的情况。
- 测试异常场景:模拟存储过程抛出异常(比如
SqlException),验证你的代码是否正确处理(比如捕获异常返回空列表,或者抛出自定义异常):
[TestMethod] [ExpectedException(typeof(SqlException))] public void AnimalsFromSP_ShouldThrowExceptionWhenSPFails() { var mockContext = new Mock<YourDbContext>(); mockContext.Setup(m => m.ExecuteStoredProcedure<Animal>("spGetAnimals")) .Throws(new SqlException()); mockContext.Object.AnimalsFromSP(); }
- 别测试EF本身:单元测试测的是你的代码逻辑,EF的SqlQuery能不能正确调用数据库那是集成测试的事,我们只需要验证你的代码有没有正确调用存储过程、处理返回值就行。
- 重构难测试的代码:如果代码直接写在控制器或者其他难以Mock的地方,把逻辑提取到仓储类或服务类里,用依赖注入的方式传入DbContext,这样测试起来会轻松很多。
内容的提问来源于stack exchange,提问作者Emiliano Rodriguez
相关产品推荐
相关产品推荐

