【问题标题】:Mock with 'dynamic' generic and anonymous object 'new {}' fails使用“动态”通用和匿名对象“新 {}”模拟失败
【发布时间】:2021-08-01 04:02:55
【问题描述】:

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

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

这是我的设置。模拟不起作用。在 AlbumData.GetAlbumsAsync() 方法中,对模拟对象 (_sql.LoadDataAsync) 的调用返回 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;
  }
}

相册数据.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&lt;string&gt;()

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

https://dotnetfiddle.net/nquthR

这一切都在小提琴中有效,但是当我将完全相同的测试粘贴到我的 AlbumDataTest 中时,它仍然返回 null。 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 类移动到与AllbumDataTest 类相同的项目中,则模拟工作(使用dynamic)返回包含三个对象的列表。但是当AlbumData 类在一个单独的项目中时(就像在现实世界中一样),mock 会返回 null。

我已更新 https://github.com/PerProjBackup/Failing-Mock 存储库。我删除了控制台应用程序并创建了一个包含两种方案的 Failing 和 Passing 文件夹。

为什么将模拟传递给不同项目的类会导致模拟失败?

更新4:

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

【问题讨论】:

    标签: c# generics mocking moq


    【解决方案1】:

    可能与异步方法有关。根据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&lt;object&gt;() 添加到设置中:

    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");
    

    操作说明/摘要:

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

    【讨论】:

    • 正如我向 freeAll 解释的那样,It.IsAny&lt;string&gt;() 是不需要的,也不能解决问题。它也与异步无关。我的代码适用于其他非泛型异步方法。
    • It.IsAny 可能不需要,但是你应该传入你期望的特定字符串,在这种情况下是"dbo.spAlbum_GetAll" 和"AlbumConnection",而不是空字符串。
    • 它是一个Mock,输入参数,超出类型,无关紧要。
    • It.IsAny 的全部意义在于只匹配类型而忽略值。这就是您告诉最小起订量参数无关紧要的方式。我也不清楚为什么要在 AlbumData.cs 中指定动态类型参数。这似乎没有必要,因为您只是传入了一个空对象。
    • @freeAll 正确,见下例https://dotnetfiddle.net/zcQPH3
    【解决方案2】:

    使用It.IsAny&lt;string&gt;() 代替空字符串

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

    注意:您不能在动态对象上使用It.IsAny&lt;T&gt;()

    【讨论】:

    • 您为什么认为这会有所作为?不需要您的更改,它仍然返回 null。
    • 我的理解是,当您的设置需要一个空字符串时,您不能传入“dbo.spAlbum_GetAll”
    猜你喜欢
    • 1970-01-01
    • 2014-02-02
    • 2012-03-09
    • 1970-01-01
    • 2019-04-15
    • 2014-04-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-06
    相关资源
    最近更新 更多