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

Wal*_*ter 5 unit-testing entity-framework in-memory-database

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

问题:

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

其他方案:

IDataContext

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

这是我最初使用的路线,我甚至编写了一个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 };
Run Code Online (Sandbox Code Playgroud)

我的解决方案

这其实很简单.我只是创建一个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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下我CollectionDbSet <>是FakeDbSet <>的一个略加修改这里(其简单地与下面的ObservableCollection和ObservableCollection.AsQueryable实现IDbSet()).

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

完整的集成测试

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

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

if(Global.RunIntegrationTest)
    DependencyInjector.Bind<MyDataContext>().To<MyDataContext>().InSingletonScope();
else
    DependencyInjector.Bind<MyDataContext>().To<InMemoryDataContext>().InSingletonScope();
Run Code Online (Sandbox Code Playgroud)

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

Wal*_*ter 3

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