Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock or better workaround for AsNoTracking in ASP.NET Core

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:

  1. Is with my workaround the only way to test stuff like this?
  2. Is there a possibility to Mock this?
  3. What does Check.NotNull(source, nameof(source)); in AsNoTracking() do?
like image 446
Pixel Lord Avatar asked Oct 24 '18 21:10

Pixel Lord


Video Answer


1 Answers

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.

like image 89
Fabio Avatar answered Oct 21 '22 12:10

Fabio