0

I am writing a unit test to test a method that gets data using Entity Framework and LINQ. I'm using mocked DbSets in my test. The method is returning data from the Orders DbSet, along with a navigation property from the related Customers DbSet, using the extension System.Data.Entity.Include method.

I've run into a weird situation in which, depending on where I call Include, either A) the test succeeds or B) I get an exception. Therein lies my question.

(This is a simplification of the actual code. I realize that these tests, as written below, are silly and pointless.)

public class Order
{
    // Primary key
    public string OrderId { get; set; }

    // Foreign key to Customers table
    public string CustomerId { get; set; }

    // Navigation property
    public virtual Customer Customer {get; set; }
}

public class Customer
{
    // Primary key
    public string CustomerId { get; set; }
}

public class MyContext : DbContext
{
    public virtual DbSet<Customer> Customers { get; set; }
    public virtual DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>()
            .HasKey(p => new { p.OrderId });

        modelBuilder.Entity<Order>()
            .HasRequired(p => p.Customer)
            .WithMany();

        modelBuilder.Entity<Customer>()
            .HasKey(p => new { p.CustomerId });
    }

    // ...
}

[TestClass]
public class MyTests
{
    // Creates a mock DbSet that can be used for Entity Framework contexts
    private static Mock<DbSet<TEntity>> CreateMockDbSet<TEntity>(IEnumerable<TEntity> models) where TEntity : class
    {
        Mock<DbSet<TEntity>> dbSet = new Mock<DbSet<TEntity>>();

        IQueryable<TEntity> queryable = models.AsQueryable();

        dbSet.As<IQueryable<TEntity>>().Setup(e => e.ElementType).Returns(queryable.ElementType);
        dbSet.As<IQueryable<TEntity>>().Setup(e => e.Expression).Returns(queryable.Expression);
        dbSet.As<IQueryable<TEntity>>().Setup(e => e.GetEnumerator()).Returns(queryable.GetEnumerator());
        dbSet.As<IQueryable<TEntity>>().Setup(e => e.Provider).Returns(queryable.Provider);

        return dbSet;
    }

    // This test succeeds
    [TestMethod]
    public void GetOrders1()
    {
        Mock<DbSet<Customer>> customersDbSet = CreateMockDbSet(new List<Customer>
        {
            new Customer { CustomerId = "12345" }
        });
        Mock<DbSet<Order>> ordersDbSet = CreateMockDbSet(new List<Order>
        {
            new Order { OrderId = "0000000001", CustomerId = "12345" }
        });
        Mock<MyContext> context = new Mock<MyContext>();
        context.Setup(e => e.Customers).Returns(customersDbSet.Object);
        context.Setup(e => e.Orders).Returns(ordersDbSet.Object);

        // This succeeds
        List<Order> orders =
            (from o in context.Object.Orders
             select o).Include(p => p.Customer).ToList();

        Assert.AreEqual(1, orders.Count);
    }

    // This test results in an exception that says "System.ArgumentNullException: Value cannot be null."
    [TestMethod]
    public void GetOrders2()
    {
        Mock<DbSet<Customer>> customersDbSet = CreateMockDbSet(new List<Customer>
        {
            new Customer { CustomerId = "12345" }
        });
        Mock<DbSet<Order>> ordersDbSet = CreateMockDbSet(new List<Order>
        {
            new Order { OrderId = "0000000001", CustomerId = "12345" }
        });
        Mock<MyContext> context = new Mock<MyContext>();
        context.Setup(e => e.Customers).Returns(customersDbSet.Object);
        context.Setup(e => e.Orders).Returns(ordersDbSet.Object);

        // This fails
        List<Order> orders =
            (from o in context.Object.Orders.Include(p => p.Customer)
             select o).ToList();

        Assert.AreEqual(1, orders.Count);
    }        
}

The two tests are identical, except for the location of the Include method in my LINQ query. I've checked this with a real database attached, and both ways of writing the query result in the same SQL being executed.

Why does the first test method succeed, but the second one results in an exception?

Ben Rubin
  • 6,909
  • 7
  • 35
  • 82
  • I imagine this is to do with the way you are mocking `IDbSet` – undefined Jan 09 '18 at 21:32
  • @LukeMcGregor What does EF do differently based on the two positions of `Include` such that the `DbSet` works in one place, but not the other? – Ben Rubin Jan 09 '18 at 21:34
  • 1
    You aren't actually calling the same method, one is on IDbSet, the other on IQueryable. EF just provides both implementations. The DbSet one probably requires an actual real dbset which you dont have – undefined Jan 09 '18 at 21:37
  • Oh, that explains it. Thanks for the response. I didn't realize that there were two different `Include` methods being called. The way that I'm mocking `DbSet` is what I've seen online, such as https://stackoverflow.com/questions/20002873/entity-framework-6-mocking-include-method-on-dbset and https://msdn.microsoft.com/en-us/library/dn314429(v=vs.113).aspx. Is there a different way you recommend? – Ben Rubin Jan 09 '18 at 21:41
  • Personally Ive always used https://www.nuget.org/packages/FakeDbSet/ – undefined Jan 10 '18 at 00:54
  • Thanks for the suggestion. I'll check it out. – Ben Rubin Jan 10 '18 at 14:06

0 Answers0