首页 > 解决方案 > 使用“动态”通用和匿名对象“新 {}”模拟失败

问题描述

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;
    }
    ...
}
    

专辑数据测试.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

这一切都在小提琴中起作用,但是当我将完全相同的测试粘贴到我的中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 ExplorerMock 运行测试会失败。

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

更新3:

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

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

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

更新4:

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

标签: c#genericsmockingmoq

解决方案


它可能与异步方法有关。根据异步方法的文档,您可以这样做

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

操作说明/摘要:

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


推荐阅读