You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单元测试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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 21:15:35