Sunday 27 February 2011

Mocking DbContext Entity Framework 4 Code First CTP5 with NSubstitute

Take an Entity Framework 4 Code First model, something like this:

   1: public class Customer
   2: {
   3:    public int Id { get; set; }
   4:    public string Name { get; set; }
   5: }
   6:  
   7: public class CustomerContext : DbContext, ICustomerContext
   8: {
   9:    public DbSet<Customer> Customers { get; set; }
  11: }


I now want to write some tests and I'm going to mock the DbContext using NSubstitute.  I first try something like this:

   1: [Test]
   2: public void can_mock_customer_context()
   3: {
   4:    var context = Substitute.For<CustomerContext>();
   5:    context.Customers.Returns(
   6:       new DbSet<Customer>(
   7:          new[]
   8:          {
   9:             new Customer {Name = "Sean"}
  10:          })
  11:       );
  12:    Assert.AreEqual(1, context.Customers.Count());
  13: }

The problem is that the DbSet constructor is internal (as of EF4 Code First CTP5).  So, let's abstract our DB access to a simple interface and replace DbSet<T> with the IQueryable<T> interface, ending up with the below:

   1: public interface ICustomerContext
   2: {
   3:    IQueryable<Customer> Customers { get; set; }
   4: }

This interface can be implemented like so:

   1: public class CustomerContext : DbContext, ICustomerContext
   2:   {
   3:      public DbSet<Customer> Customers { get; set; }
   4:      IQueryable<Customer> ICustomerContext.Customers { get { return Customers; } }
   5:   }

Now all we need to do is to use an implementation of IQueryable<T> in our mock.  I'm going to use EnumerableQuery<T> which gives me the following test that now passes:

   1: [Test]
   2: public void can_mock_customer_context()
   3: {
   4:    var context = Substitute.For<ICustomerContext>();
   5:    context.Customers.Returns(
   6:       new EnumerableQuery<Customer>(
   7:          new[]
   8:          {
   9:             new Customer {Name = "Sean"}
  10:          })
  11:       );
  12:    Assert.AreEqual(1, context.Customers.Count());
  13: }
  14:    }

I'm new to NSubstitute but it seem to be the lowest friction mocking library out there.  Just perfect for use with Entity Framework 4 Code First - certainly the lowest friction ORM there is today!

Note that we could have used the repository pattern to wrap the DbContext instead of a simple interface, the approach is almost identical.

Update: Ro Miller has an alternative approach using fakes that does a better job of surfacing IDbSet.  Check it out here: http://romiller.com/2010/09/07/ef-ctp4-tips-tricks-testing-with-fake-dbcontext/.

5 comments:

Roja said...

public class CustomerContext : DbContext
{
public DbSet Customers { get; set; }
}

can be replaced by

public class CustomerContext : DbContext
{
public IDbSet Customers { get; set; }
}

negating the need for the additional property and allowing you to mock to your hearts content!

Sean Kearon said...

@Roja - thanks for the comment. I was unaware of the IDbSet interface. However, after replacing DbSet with this interface, I cannot see a way to get NSubstitute to return the values I need. The following test raises an exception. Maybe I'm missing something here?

public interface ICustomerContext
{
IDbSet Customers { get; set; }
}

public class CustomerContext : DbContext, ICustomerContext
{
public IDbSet Customers { get; set; }
}

[Test]
public void can_mock_customer_context()
{
var customers = Substitute.For>();
var customer = new Customer();
customers.Add(customer);

var context = Substitute.For();
context.Customers.Returns(customers);
Assert.AreEqual(1, context.Customers.Count());
}

Anonymous said...

IQueryable is not a suitable substitute for DbSet b/c it doesn't include the Add method.

Sean Kearon said...

I've updated the article with a link to another approach.

Unknown said...

Alternatively, you can download my project on Codeplex. I have a working example that you can copy.
https://entityinterfacegenerator.codeplex.com/

It generates the interface files that you need for IoC purposes.