Friday, September 8, 2017

How to Mock Entity Framework when using Repository pattern and async methods

What we are testing

Imagine you are using the repository pattern or something similar and it is implemented something like this. I like this as the DbContext (InvoiceModel is a subclass of it) is not held for just the duration of this method. This is a very simple example with no parameters or corresponding Where() being used.

public class InvoiceRepository
    {
        public async Task<IEnumerable<Invoice>> GetInvoicesAync()
        {
            using (var ctx = new InvoiceModel())
            {
                return await ctx.Invoices.ToListAsync();
            }
        }
    }

The first challenge and solution

The problem with this that it is not really possible to mock out the Entity Framework because we can't get a reference to the DbContext (InvoiceModel) since it is created here. That is no problem, we can use the Factory pattern to allow us to have the advantage of holding it just for the duration of this method, but also allowing us to mock it. Here is how we might amend the above InvoiceRepository class to use the InvoiceModelFactory might be used.


public class InvoiceRepository
    {
        private IInvoiceModelFactory _invoiceModelFactory;

        public InvoiceRepository(IInvoiceModelFactory invoiceModelFactory)
        {
            _invoiceModelFactory = invoiceModelFactory;
        }


        public async Task<IEnumerable<Invoice>> GetInvoiceAsync()
        {
            using (var ctx = _invoiceModelFactory.Create())
            {
                return await ctx.Invoices.ToListAsync();
            }
        }
    }

What's the factory pattern?

The factory pattern is useful delaying the creation of an object (our InvoiceModel in this case). If we also create an interface for the factory (IInvoiceModelFactory) and also gives us the ability to change the factory in testing to create whatever kind of implementation of IInvoiceModel that we want to.

public interface IInvoiceModelFactory
    {
        InvoiceModel Create();
    }

public class InvoiceModelFactory : IInvoiceModelFactory
    {
        public InvoiceModel Create()
        {
            return new InvoiceModel();
        }
    }

Mock the Entity Framework 

When I say mock the Entity Framework, it really ends up being DbContext which is InvoiceModel in our case. I'm using NSubstitute, but any mocking framework should be able to be used. To help with mocking the entity framework I recommend using EntityFramework.NSubstitute. There are version of it for most mocking frameworks. It provides the implementation of SetupData() below.

NB. If you are using a newer version of NSubstitute than EntityFramework.NSubstitute requires you can get the source and build it yourself. It is really only 6 files.

Helpers methods for improved readability of tests

There are some methods I created to wire up the required mocks and make the tests easier to read.

private IInvoiceModelFactory GetSubstituteInvoiceModelFactory(List<Invoice> data)
{
var context = GetSubstituteContext(data);
var factory = Substitute.For<IInvoiceModelFactory>();
factory.Create().Returns(context);
return factory;
}

private InvoiceModel GetSubstituteContext(List<Invoice> data)
{
var set = GetSubstituteDbSet<Invoice>().SetupData(data);
var context = Substitute.For<InvoiceModel>();
context.Invoices.Returns(set);
return context;
}

private DbSet<TEntity> GetSubstituteDbSet<TEntity>() where TEntity : class
{
return Substitute.For<DbSet<TEntity>, IQueryable<TEntity>, IDbAsyncEnumerable<TEntity>>();
}

Writing the Test

Now it is pretty straight forward to write the actual test. We create the invoice data that would be in the database as a List of Invoices. 

[TestMethod]
public async Task GetInvoicesAsync_ReturnsInvoices()
{
//Arrange
var invoices = new List<Invoice>
{
new Invoice(),
new Invoice()
};

var factory = GetSubstituteInvoiceModelFactory(invoices);
        var invoiceRepo = new InvoiceRepository(factory);

//Act
var result = await invoiceRepo.GetInvoicesAsync();

//Assert
Assert.IsTrue(result.Any());
}

That is it. :)

No comments: