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

.NET 6中如何用xUnit+Moq在单元测试中Mock静态扩展方法

解决方案:Mock Dapper静态扩展方法(无需重构业务代码)

你遇到的问题核心是:Moq无法直接Mock静态扩展方法(Dapper的QuerySingleOrDefaultAsync本质是静态方法),但我们可以通过以下两种可行方式绕开这个限制,完成单元测试。


方案一:手动实现Fake IDbConnection(无额外依赖)

通过手动编写模拟的IDbConnection、IDbCommand和IDataReader实现类,让Dapper的扩展方法调用我们的模拟逻辑,完全不需要修改原有业务代码。

步骤1:编写Fake实现类

// 模拟IDbConnection,仅实现Dapper需要的核心方法
public class FakeDbConnection : IDbConnection
{
    private readonly IDbCommand _command;

    public FakeDbConnection(IDbCommand command) => _command = command;

    public string ConnectionString { get; set; }
    public int ConnectionTimeout => throw new NotImplementedException();
    public string Database => throw new NotImplementedException();
    public ConnectionState State => throw new NotImplementedException();

    public IDbTransaction BeginTransaction() => throw new NotImplementedException();
    public IDbTransaction BeginTransaction(IsolationLevel il) => throw new NotImplementedException();
    public void ChangeDatabase(string databaseName) => throw new NotImplementedException();
    public void Close() => throw new NotImplementedException();
    public IDbCommand CreateCommand() => _command;
    public void Dispose() { }
    public void Open() => throw new NotImplementedException();
}

// 模拟IDbCommand,实现Dapper依赖的异步读取方法
public class FakeDbCommand : IDbCommand
{
    private readonly Func<Task<IDataReader>> _executeReaderAsync;

    public FakeDbCommand(Func<Task<IDataReader>> executeReaderAsync) => _executeReaderAsync = executeReaderAsync;

    public string CommandText { get; set; }
    public int CommandTimeout { get; set; }
    public CommandType CommandType { get; set; }
    public IDbConnection Connection { get; set; }
    public IDataParameterCollection Parameters => throw new NotImplementedException();
    public IDbTransaction Transaction { get; set; }
    public UpdateRowSource UpdatedRowSource { get; set; }

    public void Cancel() => throw new NotImplementedException();
    public IDbDataParameter CreateParameter() => throw new NotImplementedException();
    public int ExecuteNonQuery() => throw new NotImplementedException();
    public IDataReader ExecuteReader() => throw new NotImplementedException();
    public IDataReader ExecuteReader(CommandBehavior behavior) => throw new NotImplementedException();
    public object ExecuteScalar() => throw new NotImplementedException();
    public void Prepare() => throw new NotImplementedException();

    public Task<IDataReader> ExecuteReaderAsync(CancellationToken cancellationToken = default)
    {
        return _executeReaderAsync();
    }
}

// 模拟IDataReader,返回预设的测试数据
public class FakeDbDataReader : IDataReader
{
    private readonly List<User> _testUsers;
    private int _currentIndex = -1;

    public FakeDbDataReader(List<User> testUsers) => _testUsers = testUsers;

    public int FieldCount => typeof(User).GetProperties().Length;
    public object this[string name] => throw new NotImplementedException();
    public object this[int i] => throw new NotImplementedException();
    public int Depth => throw new NotImplementedException();
    public bool IsClosed => throw new NotImplementedException();
    public int RecordsAffected => throw new NotImplementedException();

    public void Close() => throw new NotImplementedException();
    public void Dispose() { }
    public bool GetBoolean(int i) => throw new NotImplementedException();
    public byte GetByte(int i) => throw new NotImplementedException();
    public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) => throw new NotImplementedException();
    public char GetChar(int i) => throw new NotImplementedException();
    public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) => throw new NotImplementedException();
    public IDataReader GetData(int i) => throw new NotImplementedException();
    public string GetDataTypeName(int i) => throw new NotImplementedException();
    public DateTime GetDateTime(int i) => throw new NotImplementedException();
    public decimal GetDecimal(int i) => throw new NotImplementedException();
    public double GetDouble(int i) => throw new NotImplementedException();
    public Type GetFieldType(int i) => typeof(User).GetProperties()[i].PropertyType;
    public float GetFloat(int i) => throw new NotImplementedException();
    public Guid GetGuid(int i) => throw new NotImplementedException();
    public short GetInt16(int i) => throw new NotImplementedException();
    public int GetInt32(int i) => throw new NotImplementedException();
    public long GetInt64(int i) => throw new NotImplementedException();
    public string GetName(int i) => typeof(User).GetProperties()[i].Name;
    public int GetOrdinal(string name) => Array.FindIndex(typeof(User).GetProperties(), p => p.Name == name);
    public string GetString(int i) => throw new NotImplementedException();
    public object GetValue(int i) => typeof(User).GetProperties()[i].GetValue(_testUsers[_currentIndex]);
    public int GetValues(object[] values)
    {
        var props = typeof(User).GetProperties();
        for (int i = 0; i < props.Length; i++)
        {
            values[i] = props[i].GetValue(_testUsers[_currentIndex]);
        }
        return props.Length;
    }
    public bool IsDBNull(int i) => throw new NotImplementedException();
    public bool NextResult() => throw new NotImplementedException();
    public bool Read()
    {
        _currentIndex++;
        return _currentIndex < _testUsers.Count;
    }
}

步骤2:在测试中使用Fake类

[Fact]
public async Task GetUser_ValidUsername_ReturnsUser()
{
    // Arrange
    var testUser = new User { Id = 1, Username = "test_user", Email = "test@example.com" };
    var dataReader = new FakeDbDataReader(new List<User> { testUser });
    var command = new FakeDbCommand(() => Task.FromResult<IDataReader>(dataReader));
    var fakeConnection = new FakeDbConnection(command);

    var authService = new AuthService(fakeConnection);

    // Act
    var result = await authService.GetUserByUsernameAsync("test_user");

    // Assert
    Assert.NotNull(result);
    Assert.Equal(testUser.Username, result.Username);
}

方案二:使用第三方扩展库(简洁高效)

如果项目允许引入第三方NuGet包,可以使用Moq.Contrib.Dapper,它专门为Moq和Dapper的集成提供支持,直接Mock Dapper的扩展方法。

步骤1:安装NuGet包

Install-Package Moq.Contrib.Dapper

步骤2:编写测试用例

[Fact]
public async Task GetUser_ValidUsername_ReturnsUser()
{
    // Arrange
    var testUser = new User { Id = 1, Username = "test_user" };
    var connectionMock = new Mock<IDbConnection>();

    // 使用库的扩展方法Mock Dapper的QuerySingleOrDefaultAsync
    connectionMock.SetupDapperAsync(c => 
        c.QuerySingleOrDefaultAsync<User>(
            It.IsAny<string>(), 
            It.IsAny<object>(), 
            null, null, null))
        .ReturnsAsync(testUser);

    var authService = new AuthService(connectionMock.Object);

    // Act
    var result = await authService.GetUserByUsernameAsync("test_user");

    // Assert
    Assert.NotNull(result);
    Assert.Equal(testUser.Id, result.Id);
}

注意事项

  • 方案一无额外依赖,完全可控,但需要编写较多模拟类,适合不能引入新库的场景。
  • 方案二更简洁,但需要引入第三方包,需确认项目允许添加依赖。
  • 两种方案均无需修改原有AuthService代码,符合你无法重构的要求。

内容的提问来源于stack exchange,提问作者loo sam wong

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 09:48:14