使用 'dynamic' 通用和匿名对象 'new {}' 的模拟失败

Mock with 'dynamic' generic and anonymous object 'new {}' fails

Visual Studio2019企业版16.9.4;最小起订量 4.16.1; xunit 2.4.1; net5.0

我正在尝试对我的 AlbumData.GetAlbumsAsync() 方法进行单元测试。我模拟了 SqlDataAccess 层,它在 generic 方法中使用 Dapper 调用数据库。

这是我的设置。模拟不起作用。在 AlbumData.GetAlbumsAsync() 方法中调用模拟对象 (_sql.LoadDataAsync) returns null 并且 output 设置为 null。

谁能告诉我哪里错了?

SqlDataAccess.cs

public async Task<List<T>> LoadDataAsync<T, U>(string storedProcedure,
                              U parameters, string connectionStringName)
{
  string connectionString = GetConnectionString(connectionStringName);

  using (IDbConnection connection = new SqlConnection(connectionString))
  {
    IEnumerable<T> result = await connection.QueryAsync<T>(storedProcedure, parameters,
                        commandType: CommandType.StoredProcedure);
    List<T> rows = result.ToList();
    return rows;
  }
}

AlbumData.cs

public class AlbumData : IAlbumData
{
    private readonly ISqlDataAccess _sql;
    
    public AlbumData(ISqlDataAccess sql)
    {
      _sql = sql;
    }

    public async Task<List<AlbumModel>> GetAlbumsAsync()
    {
      var output = await _sql.LoadDataAsync<AlbumModel, dynamic>
        ("dbo.spAlbum_GetAll", new { }, "AlbumConnection");

      return output;
    }
    ...
}
    

AlbumDataTest.cs

public class AlbumDataTest
{
    private readonly List<AlbumModel> _albums = new()
    {
      new AlbumModel { Title = "Album1", AlbumId = 1 },
      new AlbumModel { Title = "Album2", AlbumId = 2 },
      new AlbumModel { Title = "Album3", AlbumId = 3 }
    };

    [Fact]
    public async Task getAlbums_returns_multiple_records_test()
    {
      Mock<ISqlDataAccess> sqlDataAccessMock = new();
      sqlDataAccessMock.Setup(d => d.LoadDataAsync<AlbumModel, dynamic>
        (It.IsAny<string>(), new { }, It.IsAny<string>()))
           .Returns(Task.FromResult(_albums));

      AlbumData albumData = new AlbumData(sqlDataAccessMock.Object);

      List<AlbumModel> actual = await albumData.GetAlbumsAsync();

      Assert.True(actual.Count == 3);
    }
    ...
}

更新1:

根据@freeAll 和@brent.reynolds 的建议,我更新了测试以使用 It.IsAny<string>()

还更新了@brent.reynolds fiddle 以实际实施单元测试:

https://dotnetfiddle.net/nquthR

它在 fiddle 中都有效,但是当我将完全相同的测试粘贴到我的 AlbumDataTest 中时,它仍然 returns 为空。 Assert.Null(actual); 通过,Assert.True(actual.Count == 3); 失败。

更新2:

我已将测试失败的项目发布到 https://github.com/PerProjBackup/Failing-Mock。 如果您 运行 API.Library.ConsoleTests 项目 Mock 工作。如果您 运行 在 API.Library.Tests 项目中使用 Test Explorer 进行测试,Mock 将失败。

@brent.reynolds 能够通过将 dynamic 泛型更改为 object 来让 Mock 工作。现在正在尝试调试 dynamic 问题。

更新3:

如果我将 AlbumData class 移动到与 AllbumDataTest class 相同的项目中,模拟工作(使用 dynamic)返回列表三个对象。但是当 AlbumData class 在一个单独的项目中(就像在现实世界中一样)时,模拟 returns null.

我更新了 https://github.com/PerProjBackup/Failing-Mock 存储库。我删除了控制台应用程序并创建了一个包含这两种情况的失败和通过文件夹。

为什么 class 将 mock 传递到另一个项目中会使 mock 失败?

更新4:

查看 brent.reynolds 接受的答案和我的评论。问题是在模拟设置中使用了匿名对象。我删除了 Failing-Mock 存储库和 dotnetfiddle.

使用It.IsAny<string>()代替空字符串

sqlDataAccessMock.Setup(d => d.LoadDataAsync<AlbumModel, dynamic>
    (It.IsAny<string>(), new { }, It.IsAny<string>())).Returns(Task.FromResult(_albums));

注意:你不能在动态对象上使用It.IsAny<T>()

可能与异步方法有关。根据documentation for async methods,你可以做

sqlDataAccessMock
  .Setup(d => d.LoadDataAsync<AlbumModel, dynamic>(
    It.IsAny<string>(), 
    new {}, 
    It.IsAny<string>())
    .Result)
  .Returns(_albums);

sqlDataAccessMock
  .Setup(d => d.LoadDataAsync<AlbumModel, dynamic>(
    It.IsAny<string>(), 
    new {}, 
    It.IsAny<string>()))
  .ReturnsAsync(_albums);

已编辑: 尝试将 It.IsAny<object>() 添加到设置中:

sqlDataAccessMock
  .Setup(d => d.LoadDataAsync<AlbumModel, object>(
    It.IsAny<string>(),
    It.IsAny<object>(),
    It.IsAny<string>()))
  .Returns(Task.FromResult(_albums));

并将GetAlbumsAsync()中的类型参数更改为:

var output = await _sql.LoadDataAsync<AlbumModel, object>(
  "dbo.spAlbum_GetAll",
  new { },
  "AlbumConnection");

OP Note/Summary:

在模拟设置中使用匿名对象 new {} 是核心问题。当正在测试的 class 和 class 在同一个项目中时它起作用,但当它们在不同的项目中时不起作用,因为它不能被重用。 It.IsAny<dynamic>() 将不起作用,因为编译器禁止在 LINQ 表达式树中使用 dynamic。 brent.reynolds 使用 object 解决了问题。