单元测试Repo类遇System.NotSupportedException:无法Mock Dapper Execute扩展方法
问题描述
我是C#及Moq新手,正通过编写单元测试学习产品。我们的数据库仓储类中有如下方法:
using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; using Dapper; using Newtonsoft.Json; using Serilog; public bool UpdateInactiveWidgets(List<Widgets> widgets) { int rowsUpdated; using (var conn = new SqlConnection(connectionString)) { rowsUpdated= conn.Execute(@"UPDATE dbo.Widgets SET [Status] = 'Inactive' WHERE ID = @ID", widgets); } return rowsUpdated == widgets.Count ? true : false; }
我尝试为该方法编写单元测试,参考其他模拟连接的帖子写了如下测试代码:
[Fact] public void Update_Inactive_Widgets() { var repo = new WidgetRepository(); var inActiveWidgets = new List<Widgets>() { //创建2个Widgets的逻辑 }; var connectionMock = new Mock<IDbConnection>(); connectionMock.Setup(m => m.Execute(It.IsAny<string>(),It.IsAny<object>(),null,null,null)).Returns(2); var result = repo.UpdateInactiveWidgets(inActiveWidgets ); Assert.Equal(inActiveWidgets.Count,result); }
但在connectionMock.Setup行触发错误:
System.NotSupportedException: 'Unsupported expression: m =>
m.Execute(It.IsAny(), It.IsAny(), null, null, null)
Extension methods (here: SqlMapper.Execute) may not be used in setup /
verification expressions.'
我正在研究相关解决方案,同时想询问:若将数据库连接改为类全局变量,把连接逻辑移到测试方法外(如下伪代码),是否能解决问题?
using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; using Dapper; using Newtonsoft.Json; using Serilog; public UpdateInactiveWidgets() { var _conn; } public Connect_db() { _conn = new SqlConnection(connectionString); } public DisConnect_db() { //关闭数据库连接的逻辑 } public bool UpdateInactiveWidgets(List<Widgets> widgets) { int rowsUpdated; //using语句是否仍适用? using (_conn) { rowsUpdated= _conn.Execute(@"UPDATE dbo.Widgets SET [Status] = 'Inactive' WHERE ID = @ID", widgets); } return rowsUpdated == widgets.Count ? true : false; }
解决方案
1. 报错核心原因
你遇到的错误本质是:Dapper的Execute是扩展方法,Moq无法直接模拟扩展方法。扩展方法本质是静态方法,Moq只能模拟接口或类的实例成员,无法拦截静态方法的调用。
2. 可行的解决思路
要解决这个问题,需要通过依赖注入和封装数据访问逻辑来解耦:
方案A:封装数据访问接口(纯单元测试)
先把Dapper的操作封装成可模拟的接口,再让仓储类依赖这个接口,避免直接操作数据库连接:
// 定义数据访问接口 public interface IWidgetDataAccess { int UpdateInactiveWidgets(List<Widgets> widgets); } // 实现真实的数据访问逻辑 public class WidgetDataAccess : IWidgetDataAccess { private readonly string _connectionString; public WidgetDataAccess(string connectionString) { _connectionString = connectionString; } public int UpdateInactiveWidgets(List<Widgets> widgets) { using (var conn = new SqlConnection(_connectionString)) { return conn.Execute(@"UPDATE dbo.Widgets SET [Status] = 'Inactive' WHERE ID = @ID", widgets); } } } // 修改仓储类,依赖IWidgetDataAccess public class WidgetRepository { private readonly IWidgetDataAccess _dataAccess; public WidgetRepository(IWidgetDataAccess dataAccess) { _dataAccess = dataAccess; } public bool UpdateInactiveWidgets(List<Widgets> widgets) { int rowsUpdated = _dataAccess.UpdateInactiveWidgets(widgets); return rowsUpdated == widgets.Count; } }
测试时直接模拟IWidgetDataAccess即可,无需关心数据库连接:
[Fact] public void Update_Inactive_Widgets() { // 准备测试数据 var testWidgets = new List<Widgets> { new Widgets { ID = 1 }, new Widgets { ID = 2 } }; // 模拟数据访问接口 var dataAccessMock = new Mock<IWidgetDataAccess>(); dataAccessMock.Setup(m => m.UpdateInactiveWidgets(testWidgets)) .Returns(testWidgets.Count); // 创建仓储实例并执行测试 var repo = new WidgetRepository(dataAccessMock.Object); var result = repo.UpdateInactiveWidgets(testWidgets); Assert.True(result); }
方案B:使用内存数据库(集成测试)
如果想验证真实的SQL逻辑,可以用内存数据库(如SQLite内存模式)替代模拟,更贴近真实场景:
[Fact] public void Update_Inactive_Widgets_IntegrationTest() { // 创建SQLite内存连接 using var conn = new SqliteConnection("Data Source=:memory:"); conn.Open(); // 创建测试表并插入数据 conn.Execute(@"CREATE TABLE Widgets (ID INT, Status VARCHAR(50))"); var testWidgets = new List<Widgets> { new Widgets { ID = 1, Status = "Active" }, new Widgets { ID = 2, Status = "Active" } }; conn.Execute("INSERT INTO Widgets (ID, Status) VALUES (@ID, @Status)", testWidgets); // 初始化仓储并执行更新 var dataAccess = new WidgetDataAccess(conn.ConnectionString); var repo = new WidgetRepository(dataAccess); var result = repo.UpdateInactiveWidgets(testWidgets); // 验证结果 Assert.True(result); var updatedWidgets = conn.Query<Widgets>("SELECT * FROM Widgets WHERE Status = 'Inactive'").ToList(); Assert.Equal(testWidgets.Count, updatedWidgets.Count); }
3. 关于全局连接的疑问
把连接改成全局变量不能解决问题,原因有两点:
- 核心矛盾未解决:Dapper的
Execute还是扩展方法,Moq依然无法模拟,报错会持续存在。 - 写法本身存在BUG:
using (_conn)会在代码块结束时自动调用Dispose()关闭连接,下次调用UpdateInactiveWidgets时,_conn已被释放,会抛出连接已关闭的异常。using仅适用于局部创建的一次性资源,不能复用全局连接对象。
内容的提问来源于stack exchange,提问作者dot
相关产品推荐
相关产品推荐

