web-dev-qa-db-ja.com

EF4「コードファースト」とリポジトリを使用した単体テスト

最新のEF4CTPでコードファーストアプローチを使用して構築した非常に単純なASP.NETMVCテストアプリの単体テストを理解しようとしています。ユニットテストやモックなどの経験はあまりありません。

これは私のリポジトリクラスです:

public class WeightTrackerRepository
{
    public WeightTrackerRepository()
    {
        _context = new WeightTrackerContext();
    }

    public WeightTrackerRepository(IWeightTrackerContext context)
    {
        _context = context;
    }

    IWeightTrackerContext _context;

    public List<WeightEntry> GetAllWeightEntries()
    {
        return _context.WeightEntries.ToList();
    }

    public WeightEntry AddWeightEntry(WeightEntry entry)
    {
        _context.WeightEntries.Add(entry);
        _context.SaveChanges();
        return entry;
    }
}

これはIWeightTrackerContextです

public interface IWeightTrackerContext
{
    DbSet<WeightEntry> WeightEntries { get; set; }
    int SaveChanges();
}

...そしてその実装、WeightTrackerContext

public class WeightTrackerContext : DbContext, IWeightTrackerContext
{
    public DbSet<WeightEntry> WeightEntries { get; set; }
}

私のテストでは、次のものがあります。

[TestMethod]
public void Get_All_Weight_Entries_Returns_All_Weight_Entries()
{
    // Arrange
    WeightTrackerRepository repos = new WeightTrackerRepository(new MockWeightTrackerContext());

    // Act
    List<WeightEntry> entries = repos.GetAllWeightEntries();

    // Assert
    Assert.AreEqual(5, entries.Count);
}

そして私のMockWeightTrackerContext:

class MockWeightTrackerContext : IWeightTrackerContext
{
    public MockWeightTrackerContext()
    {
        WeightEntries = new DbSet<WeightEntry>();
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("01/06/2010"), Id = 1, WeightInGrams = 11200 });
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("08/06/2010"), Id = 2, WeightInGrams = 11150 });
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("15/06/2010"), Id = 3, WeightInGrams = 11120 });
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("22/06/2010"), Id = 4, WeightInGrams = 11100 });
        WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("29/06/2010"), Id = 5, WeightInGrams = 11080 });
    }

    public DbSet<WeightEntry> WeightEntries { get;set; }

    public int SaveChanges()
    {
        throw new NotImplementedException();
    }
}

コンストラクターがないためにDbSet<>を作成できないため、テストデータを作成しようとすると問題が発生します。自分のコンテキストをあざけるためのアプローチ全体で、間違ったツリーを吠えているような気がします。この完全なユニットテストの初心者には、どんなアドバイスでも大歓迎です。

49
DavidGouge

コンテキストのファクトリメソッドSet()を使用してDbSetを作成しますが、単体テストでEFに依存する必要はありません。したがって、実行する必要があるのは、IDbSetインターフェイスを使用してDbSetのスタブを実装するか、MoqやRhinoMockなどのモックフレームワークの1つを使用してスタブを実装することです。独自のスタブを作成したとすると、WeightEntryオブジェクトを内部ハッシュセットに追加するだけです。

ObjectSetとIObjectSetを検索すると、EFのユニットテストについて学ぶことができます。これらは、コードの最初のCTPリリース以前のDbSetに対応するものであり、単体テストの観点からさらに多くのことが書かれています。

これは、EFコードのテスト容易性について説明しているMSDNの 優れた記事 です。 IObjectSetを使用しますが、それでも関連性があると思います。

Davidのコメントへの応答として、-コメントに収まらないため、この補遺を以下に追加します。これが長いコメント応答のベストプラクティスであるかどうかわかりませんか?

DbSet具象型ではなく、WeightEntriesプロパティからIDbSetを返すように、IWeightTrackerContextインターフェイスを変更する必要があります。次に、モックフレームワーク(推奨)または独自のカスタムスタブを使用してMockContextを作成できます。これにより、WeightEntriesプロパティからStubDbSetが返されます。

これで、IWeightTrackerContextに依存するコード(つまり、カスタムリポジトリ)も作成されます。これは、本番環境では、IWeightTrackerContextを実装するEntity FrameworkWeightTrackerContextで渡します。これは、UnityなどのIoCフレームワークを使用したコンストラクターインジェクションによって行われる傾向があります。 EFに依存するリポジトリコードをテストするには、MockContext実装を渡して、テスト対象のコードが「実際の」EFとデータベースと通信していると見なし、(うまくいけば)期待どおりに動作するようにします。変更可能な外部データベースシステムとEFへの依存関係を削除したので、単体テストでリポジトリ呼び出しを確実に検証できます。

モックフレームワークの大部分は、動作をテストするためにモックオブジェクトの呼び出しを検証する機能を提供することです。上記の例では、テストは実際にはDbSet Add機能のみをテストしていますが、MSにはそのための単体テストがあるため、心配する必要はありません。知りたいのは、アドオンDbSetの呼び出しは、必要に応じて独自のリポジトリコード内から行われたことであり、そこでMockフレームワークが登場します。

申し訳ありませんが、これは消化することがたくさんあることを知っていますが、その記事を読んでいれば、スコット・アレンが私よりもこのことを説明するのがはるかに優れているので、多くが明らかになります:)

48
Darren Lewis

Dazが言ったことに基づいて(そして彼のコメントで述べたように)、IDbSetの「偽の」実装を作成する必要があります。 CTP 4の例を見つけることができます ここ しかし、それを機能させるには、Findメソッドをカスタマイズし、Addなどの以前に無効化されたいくつかのメソッドの戻り値を追加する必要があります。

以下は、CTP5の私自身が作成した例です。

public class InMemoryDbSet<T> : IDbSet<T> where T : class
{
    readonly HashSet<T> _data;
    readonly IQueryable _query;

    public InMemoryDbSet()
    {
        _data = new HashSet<T>();
        _query = _data.AsQueryable();
    }

    public T Add(T entity)
    {
        _data.Add(entity);
        return entity;
    }

    public T Attach(T entity)
    {
        _data.Add(entity);
        return entity;
    }

    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
    {
        throw new NotImplementedException();
    }

    public T Create()
    {
        return Activator.CreateInstance<T>();
    }

    public virtual T Find(params object[] keyValues)
    {
        throw new NotImplementedException("Derive from FakeDbSet and override Find");
    }

    public System.Collections.ObjectModel.ObservableCollection<T> Local
    {
        get { return new System.Collections.ObjectModel.ObservableCollection<T>(_data); }
    }

    public T Remove(T entity)
    {
        _data.Remove(entity);
        return entity;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _data.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _data.GetEnumerator();
    }

    public Type ElementType
    {
        get { return _query.ElementType; }
    }

    public Expression Expression
    {
        get { return _query.Expression; }
    }

    public IQueryProvider Provider
    {
        get { return _query.Provider; }
    }
}
41
Merritt

おそらくエラーが発生します

 AutoFixture was unable to create an instance from System.Data.Entity.DbSet`1[...], most likely because it has no public constructor, is an abstract or non-public type.

DbSetには保護されたコンストラクターがあります。

テスト容易性をサポートするために通常採用されるアプローチは、Dbセットの独自の実装を作成します

public class MyDbSet<T> : IDbSet<T> where T : class
{

これを次のように使用します

public class MyContext : DbContext
{
    public virtual MyDbSet<Price> Prices { get; set; }
}

以下のSO質問、2nsの回答を見ると、カスタムDbSetの完全な実装を見つけることができるはずです。

EF4「コードファースト」とリポジトリを使用したユニットテスト

そうして

 var priceService = fixture.Create<PriceService>();

例外をスローしないでください。

0
Spock