Whenever we like to
develop using test-driven development approach, Unit Testing is always
essential. When it comes to .Net Core and .Net Entity Framework and Automapper it little bit different, no
matter what unit testing framework we use, we need to mock libraries. Also when
you use third party packages such as AutoMapper it becomes more difficult and
comes with lot of other issues in Unit Testing. Here is one of the issue with automapper
function “ProjectTo” I am discussing now.
In my previous article, I
explained how to make mocking Asynchronous database calls in Unit Testing
“ProjectTo” function of
Automapper Issue in Unit Testing:
So far it was good, We
made decision to use the AutoMapper to map our database entities to Business entities.
As plan of this we changed our existing function to use automapper.
Note: I assume the reader of
this article is aware of how AutoMapper works.
From my previous article example, we changed our function to use
automapper. We used TrainingTypeDto as our business entity. This business
entity is already mapped in AutoMapper Profile. E.g. CreateMap().ReverseMap();
Here is our modified
function: _mapper is instance of
Automapper.
public async
Task<string> GetTrainingTypeDescription(string code)
{
var dto = _mapper.ProjectTo
var result = await dto.FirstOrDefaultAsync(cancellationToken);
if
(trainingType != null)
return
trainingType.Descriptions.ToString();
return string.Empty;
}
|
But we had the same
error as below which we were getting while making asynchronous DB calls. After
deep dive into the issue we found that the issue was with ProjectTo function of
automapper.
System.InvalidOperationException: The provider for the source
IQueryable doesn't implement IDbAsyncQueryProvider. Only providers that
implement IDbAsyncQueryProvider can be used for Entity Framework asynchronous
operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068.
Possible Reason for failure:
If we want to test this
function with in-memory set of data, the test will fail because our ProjectTo
function of automapper tries to generate the TrainingTypeDto object which won’t
support the interfaces needed to make the asynchronous call. The reason behind
this failure is ProjectTo function failed to create the new object which
implements the IAsyncQueryProvider
interface needed for the entity framework asynchronous extension method.
We already saw how to implement
this IAsyncQueryProvider interface. We
need to make few changes so that ProjectTo function to return the proper object
which can make the help us to mock the asynchronous calls. See the changes are highlighted
in Yellow
Background in your previously created helping classes.
internal class TestAsyncEnumerable<T> :
EnumerableQuery
{
public TestAsyncEnumerable(IEnumerable
: base(enumerable)
{ }
public TestAsyncEnumerable(Expression expression)
: base(expression)
{ }
public IAsyncEnumerator
{
return GetEnumerator();
}
public IAsyncEnumerator
{
return new TestAsyncEnumerator
}
IQueryProvider
IQueryable.Provider
{
get { return new
TestAsyncQueryProvider
}
}
internal class TestAsyncEnumerator<T> :
IAsyncEnumerator
{
private readonly IEnumerator
public TestAsyncEnumerator(IEnumerator
{
_inner =
inner;
}
public ValueTask DisposeAsync()
{
_inner.Dispose();
return new ValueTask();
}
public T Current => _inner.Current;
public ValueTask<bool> MoveNextAsync()
{
return new ValueTask<bool>(_inner.MoveNext());
}
}
internal class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
private readonly IQueryProvider _inner;
internal TestAsyncQueryProvider(IQueryProvider inner)
{
_inner =
inner;
}
public IQueryable
CreateQuery(Expression expression)
{
switch (expression)
{
case MethodCallExpression m:
{
var resultType =
m.Method.ReturnType; // Should be IQueryable
var tElement =
resultType.GetGenericArguments()[0];
var queryType = typeof(TestAsyncEnumerable<>).MakeGenericType(tElement);
return
(IQueryable)Activator.CreateInstance(queryType, expression);
}
}
return new TestAsyncEnumerable
}
public IQueryable
{
return new TestAsyncEnumerable
}
public object Execute(Expression expression)
{
return _inner.Execute(expression);
}
public TResult Execute<TResult>(Expression expression)
{
return
_inner.Execute
}
public IAsyncEnumerable
{
return new TestAsyncEnumerable
}
public TResult ExecuteAsync<TResult>(Expression
expression, CancellationToken cancellationToken)
{
var expectedResultType = typeof(TResult).GetGenericArguments()[0];
var executionResult = typeof(IQueryProvider)
.GetMethod(
name:
nameof(IQueryProvider.Execute),
genericParameterCount: 1,
types: new[] { typeof(Expression) })
?.MakeGenericMethod(expectedResultType)
.Invoke(this, new[] { expression });
return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult))
?.MakeGenericMethod(expectedResultType)
.Invoke(null, new[] { executionResult });
}
}
|
There will be no change
in our Helper class which generate our mock data.
public class UnitTestHelper
{
public static Mock
{
var mockSet = new Mock
mockSet.As
.Setup(x
=> x.GetAsyncEnumerator(default))
.Returns(new TestAsyncEnumerator
mockSet.As
.Setup(x
=> x.Provider)
.Returns(new
TestAsyncQueryProvider
mockSet.As
.Setup(x
=> x.Expression)
.Returns(testData.Expression);
mockSet.As
.Setup(x
=> x.ElementType)
.Returns(testData.ElementType);
mockSet.As
.Setup(x
=> x.GetEnumerator())
.Returns(testData.GetEnumerator());
mockSet.As
.Setup(x
=> x.GetEnumerator())
.Returns(testData.GetEnumerator());
return mockSet;
}
}
|
Next Step is to create
the data (No change):
private static IQueryable
{
return new List
{
new TrainingType()
{
TrainingTypeCode = "Qa",
Descriptions = " Quality Assurance
Training",
Name
= "QA
Training"
},
new TrainingType()
{
TrainingTypeCode = "Lab",
Descriptions = "Laboratory Training.",
Name
= "Labs
Training."
}
}.AsQueryable();
}
|
Now that we have the
test data and the helper classes needed for that data to be accessed
asynchronously by Entity Framework, Lets write our test function.
So given our function GetTrainingTypeDescription(), we want to test it to make sure it works and it’s returning
the correct value.
[Fact]
public async void
When_querying_for_a_TrainingTypeDescription__returns_Description()
{
var context = new Mock
var trainingtype = GetTrainingTypes();
var cancellationToken = new CancellationToken();
context.SetupGet(c
=> c.TrainingType)
.Returns(UnitTestHelper.GetQueryableMockDbSet
context.SetupSet(c =>
c.TrainingType)
.Returns(UnitTestHelper.GetQueryableMockDbSet
// Call the
function to test
var typeHandler = new TypeHandler(context.Object);
var result = await TypeHandler.GetTrainingTypeDescription("Lab");
Assert.True(result.HasValue);
}
|
Now, Run the Test. It
will be successfully completed without errors. You can debug and see how it
works.
No comments:
Post a Comment