Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing with EF Code First DataContext

This is more a solution / work around than an actual question. I'm posting it here since I couldn't find this solution on stack overflow or indeed after a lot of Googling.

The Problem:

I have an MVC 3 webapp using EF 4 code first that I want to write unit tests for. I'm also using NCrunch to run the unit tests on the fly as I code, so I'd like to avoid backing onto an actual database here.

Other Solutions:

IDataContext

I've found this the most accepted way to create an in memory datacontext. It effectively involves writing an interface IMyDataContext for your MyDataContext and then using the interface in all your controllers. An example of doing this is here.

This is the route I went with initially and I even went as far as writing a T4 template to extract IMyDataContext from MyDataContext since I don't like having to maintain duplicate dependent code.

However I quickly discovered that some Linq statements fail in production when using IMyDataContext instead of MyDataContext. Specifically queries like this throw a NotSupportedException

var siteList = from iSite in MyDataContext.Sites
               let iMaxPageImpression = (from iPage in MyDataContext.Pages where iSite.SiteId == iPage.SiteId select iPage.AvgMonthlyImpressions).Max()
               select new { Site = iSite, MaxImpressions = iMaxPageImpression };

My Solution

This was actually quite simple. I simply created a MyInMemoryDataContext subclass to MyDataContext and overrode all the IDbSet<..> properties as below:

public class InMemoryDataContext : MyDataContext, IObjectContextAdapter
{
    /// <summary>Whether SaveChanges() was called on the DataContext</summary>
    public bool SaveChangesWasCalled { get; private set; }

    public InMemoryDataContext()
    {
        InitializeDataContextProperties();
        SaveChangesWasCalled = false;
    }

    /// <summary>
    /// Initialize all MyDataContext properties with appropriate container types
    /// </summary>
    private void InitializeDataContextProperties()
    {
        Type myType = GetType().BaseType; // We have to do this since private Property.Set methods are not accessible through GetType()

        // ** Initialize all IDbSet<T> properties with CollectionDbSet<T> instances
        var DbSets = myType.GetProperties().Where(x => x.PropertyType.IsGenericType && x.PropertyType.GetGenericTypeDefinition() == typeof(IDbSet<>)).ToList();
        foreach (var iDbSetProperty in DbSets)
        {
            var concreteCollectionType = typeof(CollectionDbSet<>).MakeGenericType(iDbSetProperty.PropertyType.GetGenericArguments());
            var collectionInstance = Activator.CreateInstance(concreteCollectionType);
            iDbSetProperty.SetValue(this, collectionInstance,null);
        }
    }

    ObjectContext IObjectContextAdapter.ObjectContext 
    {
        get { return null; }
    }

    public override int SaveChanges()
    {
        SaveChangesWasCalled = true;
        return -1;
    }
}

In this case my CollectionDbSet<> is a slightly modified version of FakeDbSet<> here (which simply implements IDbSet with an underlying ObservableCollection and ObservableCollection.AsQueryable()).

This solution works nicely with all my unit tests and specifically with NCrunch running these tests on the fly.

Full Integration Tests

These Unit tests test all the business logic but one major downside is that none of your LINQ statements are guaranteed to work with your actual MyDataContext. This is because testing against an in memory data context means you're replacing the Linq-To-Entity provider but a Linq-To-Objects provider (as pointed out very well in the answer to this SO question).

To fix this I use Ninject within my unit tests and setup InMemoryDataContext to bind instead of MyDataContext within my unit tests. You can then use Ninject to bind to an actual MyDataContext when running the integration tests (via a setting in the app.config).

if(Global.RunIntegrationTest)
    DependencyInjector.Bind<MyDataContext>().To<MyDataContext>().InSingletonScope();
else
    DependencyInjector.Bind<MyDataContext>().To<InMemoryDataContext>().InSingletonScope();

Let me know if you have any feedback on this however, there are always improvements to be made.

like image 770
Walter Avatar asked Oct 22 '22 19:10

Walter


1 Answers

As per my comment in the question, this was more to help others searching for this problem on SO. But as pointed out in the comments underneath the question there are quite a few other design approaches that would fix this problem.

like image 176
Walter Avatar answered Oct 27 '22 09:10

Walter