How do you mock AsNoTracking or is there a better workaround for this Problem?
Example:
public class MyContext : MyContextBase
{
// Constructor
public MyContext(DbContextOptions<MyContext> options) : base(options)
{
}
// Public properties
public DbSet<MyList> MyLists{ get; set; }
}
public class MyList
{
public string Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Blocked { get; set; }
}
public class MyController : MyControllerBase
{
private MyContext ContactContext = this.ServiceProvider.GetService<MyContext>();
public MyController(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
private bool isContact(string firstName, string lastName)
{
try
{
var list = this
.ContactContext
.MyLists
.AsNoTracking() // !!!Here it explodes!!!
.FirstOrDefault(entity => entity.FirstName == firstName && entity.LastName == lastName);
return list != null;
}
catch (Exception exception)
{
throws Exception;
}
return false;
}
}
My test:
using Moq;
using Xunit;
[Fact]
[Trait("Category", "Controller")]
public void Test()
{
string firstName = "Bob";
string lastName = "Baumeister";
// Creating a list with the expectad data
var fakeContacts = new MyList[]
{
new MyList() { FirstName = "Ted", LastName = "Teddy" },
new MyList() { PartnerId = "Bob", Email = "Baumeister" }
};
// Mocking the DbSet<MyList>
var dbSet = CreateMockSet(fakeContacts.AsQueryable());
// Setting the mocked dbSet in ContactContext
ContactContext contactContext = new ContactContext(new DbContextOptions<ContactContext>())
{
MyLists = dbSet.Object
};
// Mocking ServiceProvider
serviceProvider
.Setup(s => s.GetService(typeof(ContactContext)))
.Returns(contactContext);
// Creating a controller
var controller = new ContactController(serviceProvider.Object);
// Act
bool result = controller.isContact(firstName, lastName)
// Assert
Assert.True(result);
}
private Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> data)
where T : class
{
var queryableData = data.AsQueryable();
var mockSet = new Mock<DbSet<T>>();
mockSet.As<IQueryable<T>>().Setup(m => m.Provider)
.Returns(queryableData.Provider);
mockSet.As<IQueryable<T>>().Setup(m => m.Expression)
.Returns(queryableData.Expression);
mockSet.As<IQueryable<T>>().Setup(m => m.ElementType)
.Returns(queryableData.ElementType);
mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator())
.Returns(queryableData.GetEnumerator());
return mockSet;
}
Every time I run this Test, the Exception that is thrown in isContact(String firstName, String lastName) at AsNoTracking() is:
Exception.Message:
There is no method 'AsNoTracking' on type 'Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions' that matches the specified arguments
Exception.StackTrace:
at System.Linq.EnumerableRewriter.FindMethod(Type type, String name, ReadOnlyCollection'1 args, Type[] typeArgs)
at System.Linq.EnumerableRewriter.VisitMethodCall(MethodCallExpression m)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at System.Linq.EnumerableQuery'1.GetEnumerator()
at System.Linq.EnumerableQuery'1.System.Collections.Generic.IEnumerable<T>.GetEnumerator()
at My.Package.Contact.Controller.MyController.isContact(String firstName, String lastName) in C:\Users\source\repos\src\My.Package\My.Package.Contact\Controller\MyController.cs:line 31
My attempts:
Trying to mock AsNoTracking like suggested in stackoverflow: mock-asnotracking-entity-framework:
mockSet.As<IQueryable<T>>().Setup(m => m.AsNoTracking<T>())
.Returns(mockSet.Object);
results in ASP.NET Core in a System.NotSupportedException:
'Invalid setup on an extension method: m => m.AsNoTracking()' mockSet.Setup(m => m.AsNoTracking()) .Returns(mockSet.Object);
After taking a better look at Microsoft.EntityFrameworkCore EntityFrameworkQueryableExtensions EntityFrameworkCore EntityFrameworkQueryableExtensions.cs at AtNoTracking():
public static IQueryable<TEntity> AsNoTracking<TEntity>(
[NotNull] this IQueryable<TEntity> source)
where TEntity : class
{
Check.NotNull(source, nameof(source));
return
source.Provider is EntityQueryProvider
? source.Provider.CreateQuery<TEntity>(
Expression.Call(
instance: null,
method: AsNoTrackingMethodInfo.MakeGenericMethod(typeof(TEntity)),
arguments: source.Expression))
: source;
}
Since the mocked DbSet<> i provide during the test, the Provider is IQueryable the function AsNoTracking should return the input source since "source.Provider is EntityQueryProvider" is false.
The only thing I couldn't check was Check.NotNull(source, nameof(source)); since I could not find what it does? if some has a explanation or code showing what it does I would appreciate it if you could share it with me.
Workaround:
The only workaround i found in the internet is from @cdwaddell in the thread https://github.com/aspnet/EntityFrameworkCore/issues/7937 who basically wrote his own gated version of AsNoTracking(). Using the workaround leads to success, but I wouldn't want to implement it as it seems to not check for something?
public static class QueryableExtensions
{
public static IQueryable<T> AsGatedNoTracking<T>(this IQueryable<T> source) where T : class
{
if (source.Provider is EntityQueryProvider)
return source.AsNoTracking<T>();
return source;
}
}
So, my questions:
Do not mock DataContext.
DataContext is implementation details of access layer. Entity Framework Core provide two options for writing tests with DataContext dependencies without actual database.
In-Memory database - Testing with InMemory
SQLite in-memory - Testing with SQLite
Why you shouldn't mock DataContext?
Simply because with mocked DataContext
you will test only that method called in expected order.
Instead in tests your should test behaviour of the code, returned values, changed state(database updates).
When you test behaviour you will be able refactor/optimise your code without rewriting tests for every change in the code.
In case In-memory tests didn't provide required behaviour - test your code against actual database.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With