1

Visual Studio 2019 Enterprise 16.9.4; Moq 4.16.1; xunit 2.4.1; net5.0

I'm trying to unit test my AlbumData.GetAlbumsAsync() method. I mock the SqlDataAccess layer which is making a call to the DB using Dapper in a generic method.

This is my setup. The mock is not working. In the AlbumData.GetAlbumsAsync() method the call to the mocked object (_sql.LoadDataAsync) returns null and output is set to null.

Can anyone tell me what I'm dong wrong?

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

UPDATE1:

Following @freeAll and @brent.reynolds suggestions I updated the test to use It.IsAny<string>()

Also updated @brent.reynolds fiddle to actually implement a unit test:

https://dotnetfiddle.net/nquthR

It all works in the fiddle but when I paste the exact same test into my AlbumDataTest it still returns null. Assert.Null(actual); passes, Assert.True(actual.Count == 3); fails.

UPDATE2:

I've posted a project with the failing test to https://github.com/PerProjBackup/Failing-Mock. If you run the API.Library.ConsoleTests project the Mock works. If you run the tests in the API.Library.Tests project with the Test Explorer the Mock fails.

@brent.reynolds was able to get the Mock to work by changing the dynamic generic to object. Now trying to debug the dynamic issue.

UPDATE3:

If I move the AlbumData class into the same project as the AllbumDataTest class the mock works (using dynamic) returning the list with three objects. But when the AlbumData class is in a separate project (as it would be in the real world) the mock returns null.

I've updated the https://github.com/PerProjBackup/Failing-Mock repository. I deleted the console app and created a Failing and Passing folder with the two scenarios.

Why would the class that the mock is being passed to being in a different project make the mock fail?

UPDATE4:

See brent.reynolds accepted answer and my comment there. Issue was the use of an anonymous object in the the mock setup. I've deleted the Failing-Mock repository and the dotnetfiddle.

Joe
  • 4,143
  • 8
  • 37
  • 65

2 Answers2

2

It might be related to the async method. According to the documentation for async methods, You can either do

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

or

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

Edited: Try adding It.IsAny<object>() to the setup:

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

and changing the type parameter in GetAlbumsAsync() to:

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

OP Note/Summary:

The use of an anonymous object new {} in the mock setup is the central issue. It works when the test class and class being tested are in the same project but not when they are in separate projects since it cannot then be reused. It.IsAny<dynamic>() will not work because the compiler forbids dynamic inside LINQ expression trees. brent.reynolds use of object resolves the issue.

Joe
  • 4,143
  • 8
  • 37
  • 65
brent.reynolds
  • 396
  • 2
  • 12
  • As I explained to freeAll `It.IsAny()` is not needed and doesn't fix the problem. It also has nothing to do with async. My code works with other non-generic async methods. – Joe May 11 '21 at 16:27
  • 2
    `It.IsAny` might not be needed, but then you should pass in the specific string you are expecting, in this case `"dbo.spAlbum_GetAll"` and `"AlbumConnection"`, rather than empty strings. – brent.reynolds May 11 '21 at 16:35
  • It is a Mock, input parameters, beyond type, are irrelevant. – Joe May 11 '21 at 16:47
  • 1
    The entire point of `It.IsAny` is to match only on type and ignore the value. That is how you tell moq that the parameters are irrelevant. I'm also unclear on why you are specifying a dynamic type argument in AlbumData.cs. This seems unnecessary, since you're just passing in an empty object. – brent.reynolds May 11 '21 at 16:57
  • 2
    @freeAll is correct, see the example below [https://dotnetfiddle.net/zcQPH3](https://dotnetfiddle.net/zcQPH3) – brent.reynolds May 11 '21 at 17:49
  • I took a detailed look at your dotnetfiddle and it all makes sense, you and @freeAll are correct about `It.IsAny()` . I updated your fiddle to include an actual Unit test and it still works (https://dotnetfiddle.net/nquthR). But when I paste the test in my project the mock still returns null. You have helped the process along but it is still not resolved. – Joe May 12 '21 at 01:53
  • Lets remove visual studio out the picture. Can you use the dotnet cli to run that specific test? Open cmc/terminal inside the test project’s folder and enter `dotnet test --filter FullyQualifiedName= getAlbums_returns_multiple_records_test `. Check documentation [here](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test#filter-option-details) – freeAll May 12 '21 at 10:19
  • `MSBUILD : error MSB1008: Only one project can be specified.` `Switch: getAlbums_returns_multiple_records_test` Apparently a known bug: https://stackoverflow.com/questions/51850966/dotnet-test-task-fails-with-msb1008-only-one-project-can-be-specified-error I simply ran `dotnet test` and that test failed with the System.NullReferenceException – Joe May 12 '21 at 15:29
  • If you debug the test are you able to step into the LoadDataAsync() call? – brent.reynolds May 12 '21 at 18:03
  • Set breakpoint `var output = await _sql.LoadDataAsync ("dbo.spAlbum_GetAll", new { }, "AlbumConnection");` When `Step Into` goes to next line `return output;` and `output` is `null`. – Joe May 12 '21 at 19:01
  • I'm able to reproduce it in VS. Is seems that it is related to the empty object being passed in. Changing the `.Setup(...)` call to `.Setup(d => d.LoadDataAsync(It.IsAny(), It.IsAny(), It.IsAny()))`, and the type parameter in `GetAlbumsAsync()` from `dynamic` to `object` seems to work. No idea why it works in the fiddle and not VS though. – brent.reynolds May 12 '21 at 19:44
  • I was able to implement your change and get a passing test. I need to revaluate whether I need the `dynamic`. See my Update 3: with a Github repository I'm in the process of adding. – Joe May 12 '21 at 21:38
  • I don't think you need the generic type on the `parameters` argument in the first place. Dapper is just expecting an `object` as it is, so why not just make the signature `Task> LoadDataAsync(string storedProcedure, object parameters, string connectionStringName);` – brent.reynolds May 12 '21 at 23:04
1

Use It.IsAny<string>() instead of the empty strings

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

Note: you can't use It.IsAny<T>() on dynamic objects

freeAll
  • 82
  • 1
  • 4
  • Why did you think that would make any difference? Your change is not needed and it still returns null. – Joe May 11 '21 at 15:16
  • How I understand it is that you can't pass in "dbo.spAlbum_GetAll" when your setup is expecting an empty string – freeAll May 11 '21 at 16:19