使用EF Code First DataContext进行单元测试

时间:2012-06-14 11:37:08

标签: entity-framework unit-testing in-memory-database

这比实际问题更能解决/解决问题。我在这里发帖,因为我在堆栈溢出时找不到这个解决方案,或者确实经过很多谷歌搜索。

问题:

我首先使用EF 4代码创建一个MVC 3 webapp,我想编写单元测试。我也在使用NCrunch在运行代码时运行单元测试,所以我想避免在这里支持实际的数据库。

其他解决方案:

IDataContext

我发现这是创建内存datacontext的最常用方法。它实际上涉及为MyDataContext编写接口IMyDataContext,然后在所有控制器中使用该接口。这样做的一个例子是here

这是我最初使用的路线,我甚至编写了一个T4模板来从MyDataContext中提取IMyDataContext,因为我不喜欢维护重复的相关代码。

但是我很快发现在使用IMyDataContext而不是MyDataContext时,一些Linq语句在生产中失败。特别是像这样的查询抛出一个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 };

我的解决方案

这其实很简单。我只是创建一个MyInMemoryDataContext子类到MyDataContext并覆盖所有IDbSet< ..>属性如下:

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;
    }
}

在这种情况下,我的CollectionDbSet&lt;&gt;是FakeDbSet的略微修改版本&lt;&gt; here(它只使用底层的ObservableCollection和ObservableCollection.AsQueryable()实现IDbSet。)

此解决方案适用于我的所有单元测试,特别是NCrunch即时运行这些测试。

完整集成测试

这些单元测试测试所有业务逻辑,但一个主要缺点是您的LINQ语句都不能保证与您的实际MyDataContext一起使用。这是因为针对内存数据上下文的测试意味着您正在替换Linq-To-Entity提供程序,而不是Linq-To-Objects提供程序(正如在this SO问题的答案中所指出的那样)。< / p>

要解决这个问题,我在单元测试中使用Ninject并在我的单元测试中设置InMemoryDataContext来绑定而不是MyDataContext。然后,您可以在运行集成测试时使用Ninject绑定到实际的MyDataContext(通过app.config中的设置)。

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

如果您对此有任何反馈,请告诉我,但总会有所改进。

1 个答案:

答案 0 :(得分:3)

根据我在问题中的评论,这更有助于其他人在SO上搜索此问题。但正如问题中的评论所指出的那样,还有很多其他设计方法可以解决这个问题。