C# 使用“动态”泛型和匿名对象“new{}”的模拟失败

C# 使用“动态”泛型和匿名对象“new{}”的模拟失败,c#,generics,mocking,moq,C#,Generics,Mocking,Moq,Visual Studio 2019 Enterprise 16.9.4;最小起订量4.16.1;xunit 2.4.1;net5.0 我正在尝试对AlbumData.GetAlbumsAsync方法进行单元测试。我模拟了SqlDataAccess层,它在一个通用方法中使用Dapper调用DB 这是我的设置。模拟不起作用。在AlbumData.GetAlbumsAsync方法中,对模拟对象_sql.LoadDataAsync的调用返回null,并且输出设置为null 谁能告诉我我做错了什么 Sq

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

我正在尝试对AlbumData.GetAlbumsAsync方法进行单元测试。我模拟了SqlDataAccess层,它在一个通用方法中使用Dapper调用DB

这是我的设置。模拟不起作用。在AlbumData.GetAlbumsAsync方法中,对模拟对象_sql.LoadDataAsync的调用返回null,并且输出设置为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的建议,我更新了测试以使用它

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

这一切都可以在小提琴中运行,但当我将完全相同的测试粘贴到AlbumDataTest中时,它仍然返回null。Assert.Nullactual;通过,Assert.Trueactual.Count==3;失败了

更新2:

我发布了一个测试失败的项目。 如果运行API.Library.ConsoleTests项目,模拟将正常工作。如果使用测试资源管理器运行API.Library.tests项目中的测试,模拟将失败

@brent.reynolds通过将动态通用对象更改为对象,使模拟得以运行。现在尝试调试动态问题

更新3:

如果我将AlbumData类移动到与AllbumDataTest类相同的项目中,模拟将使用动态返回包含三个对象的列表。但是当AlbumData类像在现实世界中一样位于单独的项目中时,mock返回null

我已经更新了存储库。我删除了console应用程序,并创建了一个包含两个场景的失败和通过文件夹

为什么将模拟传递到另一个项目中的类会使模拟失败

更新4:

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

使用它。IsAny而不是空字符串

sqlDataAccessMock.Setup(d => d.LoadDataAsync<AlbumModel, dynamic>
    (It.IsAny<string>(), new { }, It.IsAny<string>())).Returns(Task.FromResult(_albums));
注意:您不能在动态对象上使用它。IsAny

使用它。IsAny代替空字符串

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

注意:您不能使用它。在动态对象上,它可能与异步方法有关。根据法律,你可以

sqlDataAccessMock
  .Setup(d => d.LoadDataAsync<AlbumModel, dynamic>(
    It.IsAny<string>(), 
    new {}, 
    It.IsAny<string>())
    .Result)
  .Returns(_albums);
执行部分说明/摘要:


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

它可能与异步方法有关。根据法律,你可以

sqlDataAccessMock
  .Setup(d => d.LoadDataAsync<AlbumModel, dynamic>(
    It.IsAny<string>(), 
    new {}, 
    It.IsAny<string>())
    .Result)
  .Returns(_albums);
执行部分说明/摘要:


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

你为什么认为这会有什么不同?您的更改是不需要的,它仍然返回null。我如何理解,当您的安装程序期望一个空字符串时,您不能传入dbo.spAlbum_GetAll?为什么您认为这会有任何不同?您的更改不需要,并且仍然返回null。我的理解是,当您的安装程序需要一个空字符串时,您不能传入dbo.spAlbum_GetAll,正如我向freeAll it解释的那样。IsAny是不需要的,也不能解决问题。它也与异步无关。我的代码与其他非泛型异步方法一起工作。可能不需要It.IsAny,但您应该传入所需的特定字符串,在本例中为dbo.spAlbum_GetAll和AlbumConnection,而不是空字符串。它是一个模拟的输入参数,超出了类型,IsAny的全部要点是只在类型上匹配,而忽略值。这就是你告诉最小起订量参数是无关的。我也不清楚为什么要在AlbumData.cs中指定动态类型参数。这似乎没有必要,因为您只是传入一个空对象。@freeAll是正确的,请参阅下面的示例,正如我向freeAll It解释的那样。IsAny不是必需的,也不能解决问题。它也与异步无关。我的代码与其他非泛型异步方法一起工作。可能不需要It.IsAny,但您应该传入所需的特定字符串,在本例中为dbo.spAlbum_GetAll和AlbumConnection,而不是空字符串。它是一个模拟的输入参数,超出了类型,IsAny的全部要点是只在类型上匹配,而忽略值。这就是你告诉最小起订量参数是无关的。我也不清楚为什么要指定动态类型
AlbumData.cs中的参数。这似乎没有必要,因为您只是传入一个空对象。@freeAll是正确的,请参见下面的示例
var output = await _sql.LoadDataAsync<AlbumModel, object>(
  "dbo.spAlbum_GetAll",
  new { },
  "AlbumConnection");