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

如何使用Moq.Dapper模拟QueryAsync<dynamic>?

问题描述

我有一个在事务中查询并删除指定表记录的方法:

public async Task<IEnumerable<DataTableRecordsResponse>> DeleteDataAsync(string tenantId)
{
    if (string.IsNullOrEmpty(tenantId))
    {
        throw new ArgumentNullException(nameof(tenantId));
    }

    var deletedRecords = new List<DataTableRecordsResponse>();

    try
    {
        using (IDbConnection conn = _sqlDbConnection.CreateConnection())
        {
            conn.Open();
            using (IDbTransaction transaction = conn.BeginTransaction())
            {
                try
                {
                    foreach (string table in s_allowedTableName)
                    {
                        string selectQuery = $"SELECT * FROM {table} WHERE TenantId = @tenantId;";
                        IEnumerable<dynamic> records = await _dapperWrapper.QueryAsync<dynamic>(conn, selectQuery, new { tenantId }, transaction);

                        deletedRecords.Add(new DataTableRecordsResponse(table, records));

                        if (records.Any())
                        {
                            string deleteCommand = $"DELETE FROM {table} WHERE TenantId = @tenantId; ";
                            await _dapperWrapper.ExecuteAsync(conn, deleteCommand, new { tenantId }, transaction);
                        }
                    }

                    transaction.Commit();
                }
                catch (Exception ex)
                {
                    transaction.Rollback();
                    _logger.LogException(ex);
                    throw;
                }
                finally
                {
                    _logger.LogEvent("Tenant data purge operation completed");
                }
            }
        }
    }
    catch (Exception ex)
    {
        _logger.LogException(ex);
        throw;
    }

    return deletedRecords;
}

尝试用以下测试代码模拟QueryAsync<dynamic>时,返回值始终为null:

[TestMethod]
public async Task Successful_Transaction_Should_Complete_Transaction_And_Return_Deleted_Records()
{
    //Arrange
    _sqlDbConnection.Setup(c => c.CreateConnection()).Returns(_dbMock.Object);

    _bulkOperationHelper = new BulkOperationHelper(_sqlDbConnection.Object, _loggerMock.Object);


    _dbMock.SetupDapperAsync(c => c.QueryAsync<dynamic>(It.IsAny<string>(), It.IsAny<object>(), null, null, null))
              .ReturnsAsync(_results);

    _dbMock.SetupDapper(c => c.ExecuteScalar(It.IsAny<string>(), It.IsAny<object>(), null, null, null))
            .Returns(0);

    //Act
    List<DataTableRecordsResponse> deletedRecords = await _bulkOperationHelper.TenantDecommissioningBatchDelete(_tenantId, _tables);

    //Assert
    Assert.IsNotNull(deletedRecords);
    Assert.IsTrue(deletedRecords.Count > 0); 
    Assert.IsTrue(deletedRecords.Count == 0); 
}

已知Moq.Dapper存在相关未解决问题,现需解决该模拟返回null的问题。

解决方案

方案1:用ExpandoObject构造dynamic数据并修正Mock匹配规则

Moq.Dapper对dynamic泛型方法的支持存在缺陷,同时原测试代码存在参数匹配错误:

  • 原Mock中QueryAsync的事务参数写为null,但实际方法传入了事务对象,导致Mock不匹配返回null
  • 原方法调用的是ExecuteAsync,但测试Mock的是ExecuteScalar,同样不匹配

调整后的测试代码:

// 构造dynamic测试数据
var mockRecords = new List<dynamic>();
var record = new ExpandoObject();
record.TryAdd("Id", 1);
record.TryAdd("TenantId", _tenantId);
record.TryAdd("Column1", "test-data");
mockRecords.Add(record);

// 修正QueryAsync的Mock,匹配事务参数
_dbMock.Setup(c => c.QueryAsync<dynamic>(
    It.IsAny<string>(),
    It.IsAny<object>(),
    It.IsAny<IDbTransaction>(),
    It.IsAny<int?>(),
    It.IsAny<CommandType?>()))
.ReturnsAsync(mockRecords as IEnumerable<dynamic>);

// 修正ExecuteAsync的Mock,匹配原方法调用
_dbMock.SetupDapperAsync(c => c.ExecuteAsync(
    It.IsAny<string>(),
    It.IsAny<object>(),
    It.IsAny<IDbTransaction>(),
    It.IsAny<int?>(),
    It.IsAny<CommandType?>()))
.ReturnsAsync(1);

方案2:替换dynamic为具体DTO类

如果业务允许,将dynamic替换为具体的DTO类,让Moq.Dapper的泛型支持正常工作:

// 定义通用记录DTO
public class TenantRecord
{
    public int Id { get; set; }
    public string TenantId { get; set; }
    // 根据表结构添加对应字段
}

// 修改原方法中的QueryAsync调用
IEnumerable<TenantRecord> records = await _dapperWrapper.QueryAsync<TenantRecord>(conn, selectQuery, new { tenantId }, transaction);

// 测试中正常Mock
_dbMock.SetupDapperAsync(c => c.QueryAsync<TenantRecord>(
    It.IsAny<string>(),
    It.IsAny<object>(),
    It.IsAny<IDbTransaction>(),
    It.IsAny<int?>(),
    It.IsAny<CommandType?>()))
.ReturnsAsync(new List<TenantRecord> { new TenantRecord { Id = 1, TenantId = _tenantId } });

方案3:直接Mock DapperWrapper接口

如果代码中使用了_dapperWrapper封装层,直接Mock这个接口可以完全绕过Moq.Dapper的问题:

// 假设IDapperWrapper定义
public interface IDapperWrapper
{
    Task<IEnumerable<T>> QueryAsync<T>(IDbConnection conn, string sql, object param, IDbTransaction transaction);
    Task<int> ExecuteAsync(IDbConnection conn, string sql, object param, IDbTransaction transaction);
}

// 测试中Mock该接口
_dapperWrapperMock.Setup(w => w.QueryAsync<dynamic>(
    It.IsAny<IDbConnection>(),
    It.IsAny<string>(),
    It.IsAny<object>(),
    It.IsAny<IDbTransaction>()))
.ReturnsAsync(mockRecords);

_dapperWrapperMock.Setup(w => w.ExecuteAsync(
    It.IsAny<IDbConnection>(),
    It.IsAny<string>(),
    It.IsAny<object>(),
    It.IsAny<IDbTransaction>()))
.ReturnsAsync(1);

内容的提问来源于stack exchange,提问作者Mahyar Mottaghi Zadeh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 19:18:11