5

これは、実際の質問よりも解決策/回避策です。スタックオーバーフローで、または実際に多くのグーグルの後にこのソリューションを見つけることができなかったため、ここに投稿しています。

問題:

最初に単体テストを記述したい EF 4 コードを使用する MVC 3 webapp があります。また、NCrunch を使用して、コードを書いているときに単体テストをオンザフライで実行しているので、ここで実際のデータベースに戻ることは避けたいと思います。

その他のソリューション:

IDataContext

これは、メモリ内データコンテキストを作成する最も受け入れられている方法であることがわかりました。これには、MyDataContext のインターフェイス IMyDataContext を作成し、すべてのコントローラーでインターフェイスを使用することが効果的に含まれます。これを行う例はhereです。

これは私が最初に行ったルートであり、重複した依存コードを維持する必要がないため、MyDataContext から IMyDataContext を抽出する T4 テンプレートを作成するところまで行きました。

ただし、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 でうまく機能します。

完全統合テスト

これらの単体テストはすべてのビジネス ロジックをテストしますが、1 つの大きな欠点は、実際の MyDataContext で動作することが保証されている LINQ ステートメントがないことです。これは、インメモリ データ コンテキストに対してテストするということは、Linq-To-Entity プロバイダーではなく、Linq-To-Objects プロバイダーを置き換えることを意味するためです (このSO の質問への回答で非常によく指摘されているように)。

これを修正するために、単体テスト内で Ninject を使用し、単体テスト内で MyDataContext の代わりにバインドするように InMemoryDataContext をセットアップします。次に、Ninject を使用して、(app.config の設定を介して) 統合テストを実行するときに実際の MyDataContext にバインドできます。

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

これについてフィードバックがある場合はお知らせください。ただし、常に改善の余地があります。

4

1 に答える 1