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

5
这更像是一个解决方案/解决方法,而不是一个实际的问题。我在这里发布它,因为我在Stack Overflow或者进行了大量谷歌搜索后都找不到这个解决方案。
问题:
我有一个使用EF 4 Code First的MVC 3 Web应用程序,我想为其编写单元测试。我还使用NCrunch来动态运行单元测试,所以我想避免实际连接到数据库。
其他解决方案:
IDataContext
我发现这是创建内存数据上下文的最常接受的方法。它实际上涉及编写一个名为IMyDataContext的接口,用于您的MyDataContext,然后在所有控制器中使用该接口。这样做的示例在这里。
最初我选择了这条路线,甚至编写了一个T4模板从MyDataContext中提取IMyDataContext,因为我不喜欢维护重复的依赖代码。
然而,我很快发现,与使用MyDataContext相比,使用IMyDataContext时,一些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<> 是 FakeDbSet<> 的稍微修改版本,可以在此处找到(它只是使用了基础的 ObservableCollection 和 ObservableCollection.AsQueryable() 实现了 IDbSet)。
这个解决方案非常适合所有我的单元测试,特别是在 NCrunch 上实时运行这些测试时。 完整集成测试 这些单元测试测试了所有的业务逻辑,但一个主要的缺点是你的所有 LINQ 语句都不能保证与你的实际 MyDataContext 兼容。这是因为针对内存中的数据上下文进行测试意味着你正在替换 Linq-To-Entity 提供程序,而是使用 Linq-To-Objects 提供程序(正如在这个 SO 问题的答案中非常明确地指出的那样)。
为了解决这个问题,在我的单元测试中我使用 Ninject,并设置 InMemoryDataContext 绑定代替 MyDataContext 在我的单元测试中。然后当运行集成测试时(通过 app.config 中的设置),你可以使用 Ninject 绑定到实际的 MyDataContext。
if(Global.RunIntegrationTest)
    DependencyInjector.Bind<MyDataContext>().To<MyDataContext>().InSingletonScope();
else
    DependencyInjector.Bind<MyDataContext>().To<InMemoryDataContext>().InSingletonScope();

如果您对此有任何反馈,请告诉我,然而总是可以做出改进的。


1
这里有一个关于此问题的stackoverflow问题,还有一篇文章描述了另一种解决方案。 - Steven
1
这是关于同一主题的另一个最近的问题:在扩展方法和每个查询类之间解耦 EF 查询 - Ladislav Mrnka
1个回答

3

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接