Reputation: 4183
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.
Upvotes: 1
Views: 1566
Reputation: 414
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.
Upvotes: 2
Reputation: 82
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
Upvotes: 1