6

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?
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Pixel Lord
  • 145
  • 1
  • 7
  • 4
    Easy answer: don't attempt to mock Entity Framework. Use the InMemory provider. – Camilo Terevinto Oct 24 '18 at 21:44
  • 2
    As suggested, don't mock the DbSets, mock the DbContext. See [this answer](https://stackoverflow.com/a/52810149/2309376) for how to mock the DbContext and use an InMemory database for your tests. – Simply Ged Oct 24 '18 at 22:51
  • @SimplyGed that answer you linked to does not mock the DbContext nor does it suggest to do so. There's no need to mock anything when using InMemory provider. – Brad Oct 25 '18 at 00:51
  • @Brad - Good point. I should have said it `creates a DbContext that uses an InMemoryDatabase`. Apologies for the confusion. – Simply Ged Oct 25 '18 at 00:54
  • CamiloTerevinto and SimplyGed Thank you very much this was exactly what I was looking for, but I addressed the problem wrong. – Pixel Lord Oct 30 '18 at 13:28

2 Answers2

2

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.

Fabio
  • 31,528
  • 4
  • 33
  • 72
  • Thank you, that was the solution. – Pixel Lord Oct 30 '18 at 13:30
  • I agree with the given solution, but not totally with the explanation. Yes, there is no other way (that I know of) of mocking DataContext dependencies on EF Core, other then using In-Memory databases, and yes, testing behavior is usually preferred. But having an useful tool such as mocking not available for EF code anymore is less than ideal. Sometimes, e.g. when adding unit-tests to legacy code, we just want to make EF code "happy" with mocks and get away with it. Yes, we can always properly set a in-memory database, but that's not the same. – rsenna Jan 13 '20 at 15:41
  • @rsenna, especially in legacy projects you don't want to wrap legacy code with unit-testing, instead you will use tests against actual implementation as much as possible. Because when you start refactoring legacy code you want to get quick feedback "Did I break something". With mocked DbContext tests will fail when you will change some implementation details/query and you will be forced to rewrite tests every time you decided to use `FirstOrDefault` instead of `Where()` – Fabio Jan 13 '20 at 23:16
  • 1
    @Fabio, your explanation is, again, something in the grounds of "you don't need that", and I'm telling you that I need it. Please understand that's patronizing. I agree that in a perfect world we should be testing behavior. Let me give you a concrete example: I had to migrate a code base from EF6 to EF Core, there were unit tests already using EF6 mocks, I had to rewrite everything using in-memory databases. Is it better? Perhaps. But the actual value obtained from such work was null IMO. – rsenna Jan 14 '20 at 09:14
  • @rsenna, sorry, if you felt that way, that wasn't my intention but just form of expression. You right when behaviour already covered with tests, there no purpose of rewriting them. In case when switching between different technologies, I would still write new tests but against actual database to be confident during changes. By the way, EF6 supports .NET Core and can be used in .NET Core projects. – Fabio Jan 14 '20 at 18:57
0

Old question, but some people keep getting trouble (like me some hours ago!). I'll only let a brief explanation from official docs, that are well covered about unit testing databases. .AsNoTracking() is a query functionality, as observed in its source code:

public static IQueryable AsNoTracking(this IQueryable source)
{
    var asDbQuery = source as DbQuery;
    return asDbQuery != null ? asDbQuery.AsNoTracking() : CommonAsNoTracking(source);
}

Well, as mentioned in the MSDN:

However, properly mocking DbSet query functionality is not possible, since queries are expressed via LINQ operators, which are static extension method calls over IQueryable. As a result, when some people talk about "mocking DbSet", what they really mean is that they create a DbSet backed by an in-memory collection, and then evaluate query operators against that collection in memory, just like a simple IEnumerable. Rather than a mock, this is actually a sort of fake, where the in-memory collection replaces the the real database.

There are others ways to unit testing EF Core. Using SQLite in memory is one of the bests. Please, read this topic, that is very useful for many contexts.

arakakivl
  • 199
  • 2
  • 10