Manipulating objects with DbSet<T> and IQueryable<T> with NSubstitute returns error
Asked Answered
P

1

3

I'd like to use NSubstitute to unit test Entity Framework 6.x by mocking DbSet. Fortunately, Scott Xu provides a good unit testing library, EntityFramework.Testing.Moq using Moq. So, I modified his code to be suitable for NSubstitute and it's been looking good so far, until I wanted to test DbSet<T>.Add(), DbSet<T>.Remove() methods. Here's my code bits:

public static class NSubstituteDbSetExtensions
{
  public static DbSet<TEntity> SetupData<TEntity>(this DbSet<TEntity> dbset, ICollection<TEntity> data = null, Func<object[], TEntity> find = null) where TEntity : class
  {
    data = data ?? new List<TEntity>();
    find = find ?? (o => null);

    var query = new InMemoryAsyncQueryable<TEntity>(data.AsQueryable());

    ((IQueryable<TEntity>)dbset).Provider.Returns(query.Provider);
    ((IQueryable<TEntity>)dbset).Expression.Returns(query.Expression);
    ((IQueryable<TEntity>)dbset).ElementType.Returns(query.ElementType);
    ((IQueryable<TEntity>)dbset).GetEnumerator().Returns(query.GetEnumerator());

#if !NET40
    ((IDbAsyncEnumerable<TEntity>)dbset).GetAsyncEnumerator().Returns(new InMemoryDbAsyncEnumerator<TEntity>(query.GetEnumerator()));
    ((IQueryable<TEntity>)dbset).Provider.Returns(query.Provider);
#endif

    ...

    dbset.Remove(Arg.Do<TEntity>(entity =>
                                 {
                                   data.Remove(entity);
                                   dbset.SetupData(data, find);
                                 }));

    ...

    dbset.Add(Arg.Do<TEntity>(entity =>
                              {
                                data.Add(entity);
                                dbset.SetupData(data, find);
                              });

    ...

    return dbset;
  }
}

And I created a test method like:

[TestClass]
public class ManipulationTests
{
  [TestMethod]
  public void Can_remove_set()
  {
    var blog = new Blog();
    var data = new List<Blog> { blog };

    var set = Substitute.For<DbSet<Blog>, IQueryable<Blog>, IDbAsyncEnumerable<Blog>>()
                        .SetupData(data);

    set.Remove(blog);

    var result = set.ToList();

    Assert.AreEqual(0, result.Count);
  }
}

public class Blog
{
   ...
}

The issue arises when the test method calls set.Remove(blog). It throws an InvalidOperationException with error message of

Collection was modified; enumeration operation may not execute.

This is because the fake data object has been modified when the set.Remove(blog) method is called. However, the original Scott's way using Moq doesn't result in the issue.

Therefore, I wrapped the set.Remove(blog) method with a try ... catch (InvalidOperationException ex) block and let the catch block do nothing, then the test doesn't throw an exception (of course) and does get passed as expected.

I know this is not the solution, but how can I achieve my goal to unit test DbSet<T>.Add() and DbSet<T>.Remove() methods?

Perak answered 5/12, 2014 at 3:21 Comment(0)
R
2

What's happening here?

  1. set.Remove(blog); - this calls the previously configured lambda.
  2. data.Remove(entity); - The item is removed from the list.
  3. dbset.SetupData(data, find); - We call SetupData again, to reconfigure the Substitute with the new list.
  4. SetupData runs...
  5. In there, dbSetup.Remove is being called, in order to reconfigure what happens when Remove is called next time.

Okay, we have a problem here. dtSetup.Remove(Arg.Do<T.... doesn't reconfigure anything, it rather adds a behavior to the Substitute's internal list of things that should happen when you call Remove. So we're currently running the previously configured Remove action (1) and at the same time, down the stack, we're adding an action to the list (5). When the stack returns and the iterator looks for the next action to call, the underlying list of mocked actions has changed. Iterators don't like changes.

This leads to the conclusion: We can't modify what a Substitute does while one of its mocked actions is running. If you think about it, nobody who reads your test would assume this to happen, so you shouldn't do this at all.

How can we fix it?

public static DbSet<TEntity> SetupData<TEntity>(
    this DbSet<TEntity> dbset,
    ICollection<TEntity> data = null,
    Func<object[], TEntity> find = null) where TEntity : class
{
    data = data ?? new List<TEntity>();
    find = find ?? (o => null);

    Func<IQueryable<TEntity>> getQuery = () => new InMemoryAsyncQueryable<TEntity>(data.AsQueryable());

    ((IQueryable<TEntity>) dbset).Provider.Returns(info => getQuery().Provider);
    ((IQueryable<TEntity>) dbset).Expression.Returns(info => getQuery().Expression);
    ((IQueryable<TEntity>) dbset).ElementType.Returns(info => getQuery().ElementType);
    ((IQueryable<TEntity>) dbset).GetEnumerator().Returns(info => getQuery().GetEnumerator());

#if !NET40
    ((IDbAsyncEnumerable<TEntity>) dbset).GetAsyncEnumerator()
                                            .Returns(info => new InMemoryDbAsyncEnumerator<TEntity>(getQuery().GetEnumerator()));
    ((IQueryable<TEntity>) dbset).Provider.Returns(info => getQuery().Provider);
#endif

    dbset.Remove(Arg.Do<TEntity>(entity => data.Remove(entity)));
    dbset.Add(Arg.Do<TEntity>(entity => data.Add(entity)));

    return dbset;
}
  1. The getQuery lambda creates a new query. It always uses the captured list data.
  2. All .Returns configuration calls use a lambda. In there, we create a new query instance and delegate our call there.
  3. Remove and Add only modify our captured list. We don't have to reconfigure our Substitute, because every call reevaluates the query using the lambda expressions.

While I really like NSubstitute, I would strongly recommend looking into Effort, the Entity Framework Unit Testing Tool.

You would use it like this:

// DbContext needs additional constructor:
public class MyDbContext : DbContext
{
    public MyDbContext(DbConnection connection) 
        : base(connection, true)
    {
    }
}

// Usage:
DbConnection connection = Effort.DbConnectionFactory.CreateTransient();    
MyDbContext context = new MyDbContext(connection);

And there you have an actual DbContext that you can use with everything that Entity Framework gives you, including migrations, using a fast in-memory-database.

Revels answered 14/12, 2014 at 21:2 Comment(1)
@@Revels Ahh, sorry. I just come back from holidays. :-) It works like a charm! I should learn more about the lambda expression. Thanks!Perak

© 2022 - 2024 — McMap. All rights reserved.