Mocking an EF6 DbContext

The official Entity Framework documentation has some nice tips on how to write tests against code that uses a DbContext.

It boils down to marking the DbSet properties inside the DbContext as virtual, and preferably, using the IDbSet interface rather than the actual implementation. In addition, I like to create an interface holding just the IDbSet properties and the SaveChanges() method rather than passing down the actual DbContext everywhere. This allows me to setup my mocking even further and set it up nicely via dependecy injection if needed.

public interface ISampleContext
{
  IDbSet<Post> Posts { get; set; }
  IDbSet<Comment> Comments { get; set; }
  int SaveChanges();
}

public class SampleContext : DbContext, ISampleContext
{
  public virtual IDbSet<Post> Posts { get; set; }
  public virtual IDbSet<Comment> Comments { get; set; }
  
  public override int SaveChanges()
  {
    return base.SaveChanges();
  }
}

When the documentation gets to the part about testing query scenarios it gets a little bit more complex. You create a list of the type you want to test, in my example I would have to create a List<Post> Posts, for example, and sets the mock up using an IQueryable. This works fine, but if your DbContext has a lot of properties, this starts to get cumbersome. And if you add new items to the DbContext, you also need to add them to the test classes and do the setup.

Luckily we can use some reflection magic to help us into setting this up. I am using Moq, but it shouldn’t differ much from whatever mocking framework you decide to use.

First, let’s set up the actual mock of the IDbSet property.

public Mock<IDbSet<T>> SetupCollectionAsMock<T>(ICollection<T> data) where T : class
{
  var dataQueryable = data.AsQueryable();
  var mockSet = new Mock<IDbSet<T>>();

  mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(dataQueryable.Provider);
  mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(dataQueryable.Expression);
  mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(dataQueryable.ElementType);
  mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => dataQueryable.GetEnumerator());

  mockSet.Setup(m => m.Add(It.IsAny<T>())).Callback((T item) => data.Add(item));
  mockSet.Setup(m => m.Remove(It.IsAny<T>())).Callback((T item) => data.Remove(item));

  return mockSet;
}

Next, we set up the mock of our DbContext interface.

public Mock<T> SetupDbContext<T>() where T : class
{
  var dbContext = new Mock<T>();
  dbContext.SetupAllProperties();

  var interfaceType = typeof(T);
  var interfaceIDbSetProperties = interfaceType.GetProperties()
    .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(IDbSet<>))
    .ToList();

  foreach (var propertyInfo in interfaceIDbSetProperties)
  {
    var argument = propertyInfo.PropertyType.GetGenericArguments()[0];
    var genericType = typeof(List<>).MakeGenericType(argument);
    dynamic instance = Activator.CreateInstance(genericType);
    var collectionAsMock = SetupCollectionAsMock(instance);

    propertyInfo.SetValue(dbContext.Object, collectionAsMock.Object);
  }

  return dbContext;
}

First, we instantiate an instance of a mock object for our interface. Since our interface should only contain IDbSet properties we can safely set them all up using SetupAllProperties(). Then we are going to get the list of IDbSet properties on our type, and create a List<T> instance which will hold the actual list of objects during our test run. Notice I declare the instance created by Activator.CreateInstance(genericType) as a dynamic. Since I don’t know the type of T, I can’t easily cast this to List<T> and I need the underlying ICollection<T> type further on the be able to use the queryable properties. The dynamic keyword handles this perfectly.

Finally, in our test we can simple set up a DbContext mock by calling SetupDbContext() and start working from there.

var dbContext = SetupDbContext<ISampleContext>();
var post = new Post();

dbContext.Object.Posts.Add(post);
comments powered by Disqus