Entity Framework 6を​​使用した人々のユニットテストはどうですか?


170

私はユニットテストとTDD全般から始めたばかりです。以前は手を出していたのですが、ワークフローに追加して、より優れたソフトウェアを作成することにしました。

昨日そのようなことを含めた質問をしましたが、それ自体が質問のようです。コントローラーからビジネスロジックを抽象化し、EF6を使用して特定のモデルとデータインタラクションにマッピングするために使用するサービスクラスの実装を開始するために座った。

問題は、リポジトリでEFを抽象化したくなかったため(それでも特定のクエリなどのサービスの外で利用可能になるため)、自分のサービスをテストしたい(EFコンテキストが使用される)ために、すでに自分自身をロードブロッキングしています。 。

ここで私は問題だと思いますが、これを行う意味はありますか?もしそうなら、IQueryableによって引き起こされたリークの多い抽象化と、メモリ内で作業する際のLinqプロバイダーの違いのために、ユニットテストの主題に関するLadislav Mrnkaによる多くの優れた投稿が簡単ではないことを踏まえて、人々はどうやってそれを実際にやっていますか?特定のデータベースに並置された実装。

テストしたいコードは非常に単純なようです。(これは、私が何をしているかを理解するためのダミーコードです。TDDを使用して作成を推進したいと思います)

環境

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

サービス

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

現在、私はいくつかのことをしようと考えています。

  1. このアプローチのようなものでEFコンテキストをモックする-単体テストのときにEFをモックするか、moqのようなインターフェイスでモックフレームワークを直接使用する-単体テストはパスするかもしれないが、必ずしもうまくいっていなくても、統合テストでそれらをバックアップする必要がないという苦痛を感じますか?
  2. EFをモックするためにEffortのようなものを使用しているかもしれません-私はそれを使用したことがなく、他の誰かが実際にそれを使用しているかどうかわかりませんか?
  3. EFを単にコールバックするだけのテストを行う必要はありません。本質的に、EFを直接呼び出すサービスメソッド(getAllなど)は、単体テストではなく、統合テストのみです。

レポなしで実際にこれを実行して成功している人はいますか?


ちょっとModika、私はこれについて最近考えていました(この質問のため:stackoverflow.com/questions/25977388/…)その中で、私は現在の仕事のやり方をもう少し正式に説明しようとしていますが、その方法を聞きたいですあなたはそれをやっています。
2014年

こんにちは@samy、私たちがそれを行うことを決めた方法は、EFに直接触れるものを単体テストすることではありませんでした。クエリはテストされましたが、単体テストではなく統合テストとして行われました。EFのモックは少し汚れているように感じますが、このプロジェクトは小さめで、データベースにヒットするテストの負荷によるパフォーマンスへの影響はそれほど問題ではなかったので、もう少し実用的でした。私はまだあなたに完全に真実であるための最良のアプローチが何であるか100%確信していません、いつかあなたはEF(そしてあなたのDB)にヒットするつもりであり、ユニットテストは私にとってここで正しいと感じません。
Modika 2014年

回答:


186

これは私が非常に興味を持っているトピックです。EFやNHibernateなどのテクノロジをテストすべきではないと言う純粋主義者はたくさんいます。彼らは正しいです、彼らはすでに非常に厳格にテストされており、以前の回答が述べたように、所有していないものをテストするのに膨大な時間を費やすことはしばしば無意味です。

ただし、その下にデータベースを所有しています。これは、私の意見ではこのアプローチが失敗するところです。EF/ NHが正しく機能していることをテストする必要はありません。マッピング/実装がデータベースで機能していることをテストする必要があります。私の意見では、これはテストできるシステムの最も重要な部分の1つです。

ただし厳密に言えば、単体テストの領域から統合テストに移行していますが、原則は同じです。

最初に行う必要があるのは、DALをモック化して、BLLをEFおよびSQLとは独立してテストできるようにすることです。これらはあなたの単体テストです。次に、DALを証明するために統合テストを設計する必要があります。私の意見では、これらはすべて同じくらい重要です。

考慮すべき点がいくつかあります。

  1. データベースは、テストごとに既知の状態である必要があります。ほとんどのシステムでは、このためにバックアップまたは作成スクリプトを使用します。
  2. 各テストは繰り返し可能でなければならない
  3. 各テストはアトミックである必要があります

データベースを設定するには、主に2つの方法があります。1つ目は、UnitTestのDB作成スクリプトを実行する方法です。これにより、ユニットテストデータベースは各テストの開始時に常に同じ状態になります(これをリセットするか、トランザクションで各テストを実行してこれを確認できます)。

あなたの他のオプションは私が何をするか、個々のテストごとに特定のセットアップを実行することです。これが2つの主な理由で最良のアプローチだと思います。

  • データベースはよりシンプルで、テストごとにスキーマ全体を必要としません
  • 各テストはより安全です。作成スクリプトで1つの値を変更しても、他の何十ものテストが無効になることはありません。

残念ながら、ここでの妥協はスピードです。これらのすべてのテストを実行し、これらのすべてのセットアップ/破棄スクリプトを実行するには時間がかかります。

最後のポイントとして、ORMをテストするためにこのような大量のSQLを作成することは非常に困難な作業になる可能性があります。これは私が非常に厄介なアプローチをするところです(ここの純粋主義者は私に同意しません)。ORMを使用してテストを作成します!システムのDALテストごとに個別のスクリプトを作成するのではなく、オブジェクトを作成し、コンテキストにアタッチして保存するテストセットアップフェーズがあります。次に、テストを実行します。

これは理想的なソリューションとはほど遠いですが、実際には管理が非常に簡単であることがわかります(特に数千のテストがある場合)。そうしないと、大量のスクリプトを作成することになります。純度よりも実用性。

私のアプローチが変わったとき、私は間違いなく数年(月/日)でこの回答を振り返り、自分自身に同意しません。ただし、これは私の現在のアプローチです。

上記で述べたすべてを試してまとめると、これは私の典型的なDB統合テストです。

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

ここで注目すべき重要な点は、2つのループのセッションが完全に独立していることです。RunTestの実装では、コンテキストがコミットおよび破棄され、データが2番目の部分のデータベースからのみ取得できることを確認する必要があります。

2014年10月13日編集

私はおそらくこのモデルを今後数ヶ月にわたって修正するだろうと言っていました。私は上記で提唱したアプローチを大部分は支持していますが、テストメカニズムを少し更新しました。TestSetupとTestTearDownでエンティティを作成する傾向があります。

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

次に、各プロパティを個別にテストします

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

このアプローチにはいくつかの理由があります。

  • 追加のデータベース呼び出しはありません(1つのセットアップ、1つのティアダウン)
  • テストははるかに詳細であり、各テストは1つのプロパティを検証します
  • Setup / TearDownロジックがTestメソッド自体から削除されました

これにより、テストクラスが単純になり、テストがより詳細になります(単一のアサートで十分です)。

2015年5月3日を編集

このアプローチの別の改訂。クラスレベルのセットアップは、プロパティの読み込みなどのテストに非常に役立ちますが、さまざまなセットアップが必要な場合にはあまり役に立ちません。この場合、ケースごとに新しいクラスを設定するのはやりすぎです。

これを支援するために、私は2つの基本クラスSetupPerTestとを使用する傾向がありSingleSetupます。これらの2つのクラスは、必要に応じてフレームワークを公開します。

SingleSetupは、最初の編集で説明したのと非常によく似たメカニズムがあります。例は

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

ただし、正しいエンティティのみが読み込まれることを保証する参照では、SetupPerTestアプローチを使用できます。

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

要約すると、どちらのアプローチも、テストする対象に応じて機能します。


2
次に、統合テストへの別のアプローチを示します。TL; DR-アプリケーション自体を使用してテストデータをセットアップし、テストごとにトランザクションをロールバックします。
Gert Arnold

3
@Liath、素晴らしい反応。EFのテストに対する私の疑惑を確認しました。私の質問はこれです。あなたの例は非常に具体的なケースであり、それは問題ありません。ただし、既に述べたように、何百ものエンティティをテストする必要がある場合があります。毎回同じ基本的なコードパターンを繰り返さずに、DRYの原則(Do not Repeat Yourself)に従って、ソリューションをどのようにスケーリングしますか?
Jeffrey A. Gochin

4
これは問題を完全に回避するため、私はこれに同意する必要があります。単体テストとは、関数のロジックをテストすることです。OPの例では、ロジックはデータストアに依存しています。EFをテストしないと言うのは正しいですが、それは問題ではありません。問題は、データストアから分離してコードをテストすることです。マッピングのテストは、まったく別のトピックです。ロジックがデータと正しく相互作用していることをテストするには、ストアを制御できる必要があります。
Sinaesthetic 16

7
Entity Frameworkを単体で単体テストする必要があるかどうかについて、誰も垣間見ることができません。何が行われるか、また偶然にデータベースへのEF呼び出しを行うメソッドをテストする必要があるということです。ビルドサーバーにデータベースを必要とせずにこのメソッドをテストできるように、EFをモックすることが目標です。
マフィンマン

4
私は旅が本当に好きです。時間の経過とともに編集を追加していただきありがとうございます。これは、ソース管理を読み、思考がどのように進化したかを理解するようなものです。機能(EF付き)とユニット(モックEF)の区別も本当にありがたいです。
Tom Leys 2016年

21

努力経験のフィードバックはこちら

何度も読んだ後、テストでEffortを使用しました。テスト中、コンテキストは、メモリ内バージョンを返すファクトリによって構築されます。これにより、毎回空白のスレートに対してテストできます。テスト以外では、ファクトリーはコンテキスト全体を返すものに解決されます。

ただし、データベースのフル機能のモックに対するテストでは、テストが下にドラッグされる傾向があると感じています。システムの一部をテストするためには、一連の依存関係をすべてセットアップする必要があることに気づくでしょう。また、すべてを処理する巨大なオブジェクトが1つしかないため、関連しない可能性のあるテストをまとめて整理する傾向があります。注意を払わないと、単体テストの代わりに統合テストを実行していることに気付くかもしれません

巨大なDBContextではなく、より抽象的なものに対してテストすることを望んでいましたが、意味のあるテストと必要最小限のテストの間にスイートスポットを見つけることができませんでした。私の未経験までそれをチョークで書きます。

だから私は努力を興味深いと思います。実際に実行する必要がある場合は、すぐに始めて結果を得るための優れたツールです。しかし、もう少しエレガントで抽象的なものを次のステップにすべきだと私は思います。それが私が次に調査するものです。この投稿をお気に入りに追加して、次に進む方向を確認します:)

編集して追加:ウォームアップには少し時間がかかるので、約。テスト起動時に5秒。テストスイートを非常に効率的にする必要がある場合、これは問題になることがあります。


明確にするために編集:

Effortを使用してWebサービスアプリをテストしました。入力された各メッセージMは、IHandlerOf<M>ウィンザー経由でルーティングされます。Castle.Windsor IHandlerOf<M>は、コンポーネントの依存関係を解決するを解決します。これらの依存関係の1つはDataContextFactory、ハンドラーがファクトリーを要求できるようにするです

テストでは、IHandlerOfコンポーネントを直接インスタンス化し、SUTのすべてのサブコンポーネントをモックDataContextFactoryし、ハンドラーにラップされたEffortを処理します。

これは、DBがテストの影響を受けるため、厳密な意味での単体テストは行わないことを意味します。しかし、上で言ったように、それは私が実際に実行することを可能にし、アプリケーションのいくつかのポイントをすばやくテストすることができました


入力のおかげで、このプロジェクトを実行するために私がしなければならないことがあるので、私は何をすることができますか?アプリケーションでどのレイヤーに労力を費やしていますか?
Modika、2014年

2
エフォートがトランザクションを適切にサポートしていた場合のみ
セダトカパノグル2014

文字列でnullの代わりに ''を使用すると、csvローダーで文字列にバグが発生します。
Sam

13

コードを単体テストする場合は、テストするコード(この場合はサービス)を外部リソース(データベースなど)から分離する必要があります。おそらくこれをある種のインメモリEFプロバイダーで行うことができますが、より一般的な方法は、EF実装を抽象化することです。たとえば、ある種のリポジトリパターンを使用します。この分離がない場合、作成するテストは単体テストではなく統合テストになります。

EFコードのテストについて-初期化中にさまざまな行をデータベースに書き込むリポジトリの自動統合テストを作成し、リポジトリの実装を呼び出して期待どおりに動作することを確認します(たとえば、結果が正しくフィルターされていることを確認する、またはそれらが正しい順序でソートされていること)。

これらは単体テストではなく統合テストです。テストはデータベース接続の存在に依存しており、ターゲットデータベースにはすでに最新の最新のスキーマがインストールされているためです。


@justinに感謝します。Repositoryパターンについて知っていますが、ayende.com / blog / 4784 / lostechies.com/jimmybogard/2009/09/11/wither-the-repositoryのようなものを読んだことで、私はそうではないと思いましたこの抽象化レイヤーは必要ありませんが、繰り返しますが、これらは非常に混乱するクエリアプローチについても説明しています。
Modika 2014年

7
@Modika Ayendeは、批評するリポジトリパターンの貧弱な実装を選択しました。その結果、100%正解です-過度に設計されていて、何のメリットもありません。適切な実装は、コードの単体テスト可能な部分をDAL実装から分離します。NHibernateとEFを直接使用すると、コードの単体テストが(不可能ではないにしても)困難になり、厳格なモノリシックコードベースが作成されます。私はまだリポジトリパターンにいくらか懐疑的ですが、DALの実装を何らかの方法で分離する必要があると100%確信しており、リポジトリはこれまでのところ最高のものです。
ジャスティン

2
@Modika 2番目の記事をもう一度お読みください。「私はこの抽象化層が欲しくない」と彼が言うことではありません。さらに、Fowler(martinfowler.com/eaaCatalog/repository.html)またはDDD(dddcommunity.org/resources/ddd_terms)の元のリポジトリパターンについて読んでください。元の概念を完全に理解せずに否定者を信じないでください。彼らが実際に批判しているのは、パターン自体ではなく、最近のパターンの誤用です(ただし、おそらくこれを知らないでしょう)。
guillaume31 2014年

1
@ guillaume31私はリポジトリパターンに反対ではありません(理解しています)私は、そのレベルですでに抽象化されているものを抽象化する必要があるかどうか、そしてそれを省略してモックによって直接EFに対してテストできるかどうかを理解しようとしていますアプリケーションの上位層でテストに使用します。さらに、リポジトリを使用しない場合、EF拡張機能セットの利点を利用できますが、リポジトリではそれを利用できない場合があります。
Modika、2014年

リポジトリを使用してDALを分離したら、データベース(EF)を「モック」する必要があります。これまでのところ、コンテキストとさまざまな非同期拡張機能(ToListAsync()、FirstOrDefaultAsync()など)をあざけると、私にとってフラストレーションが生じます。
ケビンバートン

9

Entity Frameworkは実装であるため、データベースの相互作用の複雑さを抽象化しているにもかかわらず、直接の相互作用は依然として密結合であり、そのため、テストが混乱しています。

単体テストとは、関数のロジックとその潜在的な結果のそれぞれを、外部依存関係(この場合はデータストア)から切り離してテストすることです。そのためには、データストアの動作を制御できる必要があります。たとえば、フェッチしたユーザーが基準のセットを満たさない場合に関数がfalseを返すことをアサートする場合、[モック]データストアは、基準を満たさないユーザーを常に返すように構成する必要があります。反対のアサーションの場合はその逆です。

そうは言っても、EFが実装であるという事実を受け入れれば、私はリポジトリを抽象化するという考えを支持するでしょう。少し冗長に見えますか?コードをデータ実装から分離する問題を解決しているので、そうではありません。

DDDでは、リポジトリーはDAOではなく、集約ルートのみを返します。このようにして、リポジトリのコンシューマーはデータ実装について知る必要はありません(そうするべきではありません)。この問題を解決する方法の例としてそれを使用できます。この場合、EFによって生成されるオブジェクトはDAOであるため、アプリケーションから非表示にする必要があります。これは、定義するリポジトリのもう1つの利点です。EFオブジェクトではなく、ビジネスオブジェクトを戻り値の型として定義できます。ここで、リポジトリが行うことは、EFへの呼び出しを非表示にし、EF応答をリポジトリ署名で定義されたそのビジネスオブジェクトにマップすることです。これで、クラスに注入するDbContext依存関係の代わりにそのリポジトリを使用できるようになり、その結果、そのインターフェースをモックして、コードを分離してテストするために必要なコントロールを提供できるようになりました。

これは少し手間がかかり、多くの人が鼻をなでますが、実際の問題を解決します。別の回答で言及されたインメモリプロバイダーがオプションである可能性があり(私は試していません)、その存在自体が実践の必要性の証拠です。

それはあなたのコードを分離するという本当の問題を回避し、そしてあなたのマッピングのテストについての正接に行くので、私は完全に上の答えに同意しません。必要な場合は必ずマッピングをテストしてください。ただし、ここで実際の問題に対処し、実際のコードカバレッジを取得してください。


8

所有していないコードの単体テストは行いません。MSFTコンパイラーが機能することをここでテストしていますか?

つまり、このコードをテスト可能にするには、データアクセスレイヤーをビジネスロジックコードから分離する必要があります。私が行うことは、EFのすべてのものを取り、対応するインターフェイスも持つ(または複数の)DAOまたはDALクラスに配置することです。次に、インターフェイスとして参照される依存関係(できればコンストラクターの注入)としてDAOまたはDALオブジェクトが注入されるサービスを作成します。これで、テストが必要な部分(コード)は、DAOインターフェイスをモックアウトし、ユニットテスト内のサービスインスタンスに挿入することで簡単にテストできます。

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

ライブデータアクセスレイヤーは、単体テストではなく統合テストの一部と見なします。以前にデータベースが休止状態になっているデータベースへのトリップ回数を検証する人を見たことがありますが、彼らはデータストアに何十億ものレコードが含まれるプロジェクトに参加しており、それらの追加のトリップが本当に重要でした。


1
回答をありがとうございます。しかし、このレベルでEFの内部を隠しているリポジトリと言うと、これの違いは何でしょうか?EFを抽象化したくありませんが、IContextインターフェイスを使用してまだそれを行っている可能性がありますか?私はこれに
慣れてい

3
@Modikaレポも結構です。あなたが望むどんなパターンでも。「EFを抽象化したくない」テスト可能なコードが必要かどうか。
ジョナサンヘンソン2014年

1
@Modika私の要点は、懸念事項を分離しないと、テスト可能なコードがないことです。データアクセスとビジネスロジックは、適切な保守可能なテストを実現するために、別々のレイヤーに存在する必要があります。
ジョナサンヘンソン2014年

2
本質的にはIDbSetはリポジトリとUOWのコンテキストなので、EFをリポジトリの抽象化でラップする必要がないと感じただけです。誤解を招く可能性があるので、質問を少し更新します。この問題には抽象化が伴いますが、要点は、クエリが同じ境界(linq-to-entitiesとlinq-to-objects)で実行されないため、正確に何をテストしているのかということです。それは少し無駄に思えるか、私はここで十分ですか?
Modika 2014年

1
、私はあなたの一般的な点に同意しますが、DbContextは作業の単位であり、IDbSetsは間違いなくリポジトリ実装の一部であり、私がそう思うのは私だけではありません。EFをモックすることができますが、一部のレイヤーでは統合テストを実行する必要がありますが、リポジトリで実行する場合でも、サービスでさらに上位に実行する場合でも、本当に問題になりますか?DBと緊密に結合されていることは特に問題ではありませんが、それが発生することは確かですが、発生しない可能性のある何かを計画するつもりはありません。
Modika、2014年

8

私はこれらの考慮事項に到達するためにいつかいじりました:

1-アプリケーションがデータベースにアクセスするのに、なぜテストを行うべきではないのですか?データアクセスに問題がある場合はどうなりますか?テストは事前にそれを知っていて、問題について自分に警告する必要があります。

2-リポジトリパターンはやや難しく、時間がかかります。

だから私はこのアプローチを思いつきました、私は最高だとは思いませんが、私の期待を満たしました:

Use TransactionScope in the tests methods to avoid changes in the database.

それを行うにはそれが必要です:

1- EntityFrameworkをテストプロジェクトにインストールします。2-接続文字列をテストプロジェクトのapp.configファイルに入れます。3-テストプロジェクトでdll System.Transactionsを参照します。

一意の副作用は、トランザクションが中止された場合でも、挿入しようとするとIDシードが増加することです。ただし、テストは開発データベースに対して行われるため、これは問題になりません。

サンプルコード:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}

1
実際、私はこのソリューションがとても気に入っています。実装が非常に簡単で、より現実的なテストシナリオ。ありがとう!
slopapa

1
EF 6では、DbContext.Database.BeginTransactionを使用しますね。
SwissCoder 2016

5

一言で言えば、モデルデータを取得する単一の行でサービスメソッドをテストするために、ジュースを絞る価値はありません。私の経験では、TDDを初めて使用する人は、すべてを完全にテストしたいと考えています。ファサードをサードパーティのフレームワークに抽象化する古い栗は、ダミーデータを注入できるようにバスターダイズ/拡張するフレームワークAPIのモックを作成できるため、私の心にはほとんど価値がありません。ユニットテストの最適性については、誰もが異なる見解を持っています。私は最近、より実用的になる傾向があり、私のテストが最終製品に本当に付加価値を与えるか、そしてどのようなコストであるかを自問しています。


1
はい、実用主義です。ユニットテストの品質は、元のコードの品質よりも劣ると私はまだ主張しています。もちろん、コーディング手法を改善し、保守性を向上させるためにTDDを使用することには価値がありますが、TDDの価値は減少する可能性があります。すべてのテストをデータベースに対して実行します。EFとテーブル自体の使用が適切であることを確信できるからです。テストの実行には時間がかかりますが、信頼性は高くなります。
サベージ

3

EFベースのサービスの単体テストを支援するために現在使用している実際の例について、コメントして簡単に説明したアプローチを共有したいと思います。

まず、EF Coreのインメモリプロバイダーを使用したいのですが、これはEF 6についてです。さらに、RavenDBなどの他のストレージシステムの場合、インメモリデータベースプロバイダーを介したテストも提案します。繰り返しますが、これは特に、多くの儀式なしで EFベースのコードをテストするのに役立ちます。

パターンを思いついたときの目標は次のとおりです。

  • チームの他の開発者が理解するのは簡単でなければなりません
  • EFコードを可能な限り最小限のレベルで分離する必要がある
  • 奇妙な複数責任インターフェース(「汎用」または「典型的」リポジトリパターンなど)の作成を含むことはできません。
  • 単体テストで簡単に構成および設定できる必要があります

EFはまだ実装の詳細であり、「純粋な」単体テストを行うためにEFを抽象化する必要があると感じてもかまわないという以前の記述に同意します。また、EFコード自体が機能することを確認したいのも理想的ですが、これにはサンドボックスデータベースやインメモリプロバイダーなどが含まれます。私のアプローチは両方の問題を解決します。EF依存コード安全に単体テストして作成できます。 EFコードを具体的にテストする統合テスト。

私がこれを達成した方法は、EFコードを専用のクエリクラスとコマンドクラスに単純にカプセル化することでした。考え方は簡単です。EFコードをクラスにラップし、それを最初に使用したクラスのインターフェイスに依存するだけです。私が解決する必要があった主な問題は、クラスに多数の依存関係を追加したり、テストで多くのコードを設定したりすることを避けることでした。

ここで役立つのが、シンプルなライブラリMediatrです。単純なインプロセスメッセージングが可能で、コードを実装するハンドラーからの「要求」を分離することで実現します。これには、「方法」から「内容」を切り離すという追加の利点があります。たとえば、EFコードを小さなチャンクにカプセル化することで、実装を別のプロバイダーまたは完全に異なるメカニズムに置き換えることができます。これは、アクションを実行するためのリクエストを送信するだけであるためです。

依存関係注入(フレームワークの有無にかかわらず、好み)を利用して、メディエーターを簡単にモックし、要求/応答メカニズムを制御して、EFコードの単体テストを有効にすることができます。

まず、テストする必要があるビジネスロジックを持つサービスがあるとします。

public class FeatureService {

  private readonly IMediator _mediator;

  public FeatureService(IMediator mediator) {
    _mediator = mediator;
  }

  public async Task ComplexBusinessLogic() {
    // retrieve relevant objects

    var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
    // normally, this would have looked like...
    // var results = _myDbContext.DbObjects.Where(x => foo).ToList();

    // perform business logic
    // ...    
  }
}

このアプローチの利点を理解し始めましたか?すべてのEF関連のコードを説明的なクラスに明示的にカプセル化するだけでなく、このリクエストが「どのように」処理されるかという実装の懸念を取り除くことにより、拡張性を可能にします。このクラスは、関連オブジェクトがEF、MongoDB、またはテキストファイル。

MediatRを介したリクエストとハンドラの場合:

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
  // no input needed for this particular request,
  // but you would simply add plain properties here if needed
}

public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
  private readonly IDbContext _db;

  public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
    _db = db;
  }

  public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
    return _db.DbObjects.Where(foo => bar).ToList();
  }
}

ご覧のとおり、抽象化は単純でカプセル化されています。まただ、絶対にテスト可能な統合テストでは、あなたはので可能性があり、個別にこのクラスをテストする-ここで混合一切のビジネス上の問題はありません。

では、機能サービスの単体テストはどのように見えるのでしょうか。とても簡単です。この場合、私はモックを行うためにMoqを使用しています(あなたを幸せにするものを何でも使用してください):

[TestClass]
public class FeatureServiceTests {

  // mock of Mediator to handle request/responses
  private Mock<IMediator> _mediator;

  // subject under test
  private FeatureService _sut;

  [TestInitialize]
  public void Setup() {

    // set up Mediator mock
    _mediator = new Mock<IMediator>(MockBehavior.Strict);

    // inject mock as dependency
    _sut = new FeatureService(_mediator.Object);
  }

  [TestCleanup]
  public void Teardown() {

    // ensure we have called or expected all calls to Mediator
    _mediator.VerifyAll();
  }

  [TestMethod]
  public void ComplexBusinessLogic_Does_What_I_Expect() {
    var dbObjects = new List<DbObject>() {
      // set up any test objects
      new DbObject() { }
    };

    // arrange

    // setup Mediator to return our fake objects when it receives a message to perform our query
    // in practice, I find it better to create an extension method that encapsulates this setup here
    _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
    (GetRelevantDbObjectsQuery message, CancellationToken token) => {
       // using Moq Callback functionality, you can make assertions
       // on expected request being passed in
       Assert.IsNotNull(message);
    });

    // act
    _sut.ComplexBusinessLogic();

    // assertions
  }

}

必要なのは単一のセットアップだけであり、追加の設定も不要です。これは非常に単純な単体テストです。明確にしましょう:これはMediatrのようなものなしで完全に実行できます(インターフェイスを実装してテスト用にモックするなどIGetRelevantDbObjectsQuery)が、実際には多くの機能とクエリ/コマンドを備えた大規模なコードベースの場合、カプセル化と生来のDIサポートMediatrオファー。

これらのクラスをどのように整理するか疑問に思っている場合、それは非常に簡単です。

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

機能のスライスごとに整理することは重要ではありませんが、これによりすべての関連する/依存するコードがまとめられ、簡単に見つけられるようになります。最も重要なのは、コマンドとクエリの分離の原則に従って、クエリとコマンドを分離することです。

これは私の基準をすべて満たしています。式典が低く、理解しやすく、隠れた利点があります。たとえば、変更の保存をどのように処理しますか?これで、ロールインターフェイスを使用してDbコンテキストを簡略化できます(IUnitOfWork.SaveChangesAsync())と単一の役割のインターフェースへの模擬呼び出し、またはRequestHandler内でコミット/ロールバックをカプセル化することができます。ただし、保守可能であるかぎり、それを行うのはあなた次第です。たとえば、EFオブジェクトを渡すだけで、それを保存/更新/削除する単一の汎用リクエスト/ハンドラーを作成したいと思ったのですが、意図を尋ねて、ハンドラーを別のストレージプロバイダー/実装と交換します。おそらく、意図することを表す明示的なコマンド/クエリを作成する必要があります。多くの場合、単一のサービスまたは機能には特定のものが必要になります。必要になる前に一般的なものを作成しないでください。

もちろん、このパターンには注意点があります-単純なpub / subメカニズムでは行き過ぎることがあります。私は実装をEF関連のコードの抽象化のみに制限しましたが、冒険的な開発者はMediatRを使い始めて、すべてを使いこなし、メッセージ化することができます-優れたコードレビュープラクティスとピアレビューがキャッチすべき何か。これはプロセスの問題であり、MediatRの問題ではないので、このパターンの使用方法を理解してください。

EFのユニットテスト/モックの具体的な例が必要でしたが、これは私たちのプロジェクトでうまく機能しているアプローチであり、チームは導入がいかに簡単であるかに非常に満足しています。これが役に立てば幸いです!プログラミングのすべてのものと同様に、複数のアプローチがあり、すべては達成したいことに依存します。シンプルさ、使いやすさ、保守性、発見性を重視し、このソリューションはこれらすべての要求を満たします。


答えてくれてありがとう。Mediatorを使用したQueryObjectパターンのすばらしい説明であり、プロジェクトにも取り入れ始めています。私は質問を更新する必要があるかもしれませんが、EFの単体テストはもう行っていません。抽象化があまりにも漏れているため(SqlLiteでも大丈夫かもしれません)、データベースをクエリするものと単体テストのビジネスルールやその他のロジックを統合テストします。
Modika

3

インメモリエンティティフレームワークデータベースプロバイダーであるEffortがあります。私は実際には試していません...これは質問で言及されていることに気づきました!

または、メモリ内データベースプロバイダーが組み込まれているEntityFrameworkCoreに切り替えることもできます。

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

コンテキストを取得するためにファクトリーを使用したので、その使用に近いコンテキストを作成できます。これはビジュアルスタジオではローカルに機能しているようですが、私のTeamCityビルドサーバーでは機能していません。

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

こんにちはアンドリュー、問題はコンテキストを取得することはありませんでした。私たちが行っていたコンテキストをファクトリーアウトして、コンテキストを抽象化し、ファクトリーによって構築させることができます。最大の問題は、メモリにあるものとLinq4Entitiesが何をするかの一貫性でした。それらは同じではなく、誤解を招くテストにつながる可能性があります。現在、私達は単に統合テストデータベースのものであり、あなたを気にする誰にとっても最良のプロセスではないかもしれません。
Modika 2017

このMoqヘルパーは、モックアウトするコンテキストがある場合に機能します(codeproject.com/Tips/1045590/…)。モックアウトされたコンテキストをリストでバッキングする場合、SQLデータベースがバッキングするコンテキストのように動作しない可能性があります。
アンドリューパテ2017

2

私は自分のフィルターをコードの他の部分から分離し、私のブログ(http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html)で概説しているように、それらをテストしたい

つまり、テストされているフィルターロジックは、LINQ式とT-SQLなどの基になるクエリ言語との間の変換が原因で、プログラムの実行時に実行されるフィルターロジックと同一ではありません。それでも、これにより、フィルターのロジックを検証できます。レイヤー間の統合をテストするまで、発生する変換や、大文字と小文字の区別やnull処理などについてはあまり心配しません。


0

エンティティフレームワークに期待することをテストする(つまり、期待を検証する)ことが重要です。私が正常に使用したこれを行う1つの方法は、この例に示すようにmoqを使用することです(この回答に長い間コピーするため)。

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

ただし注意してください... linqクエリに適切な "OrderBy"がない限り、SQLコンテキストは特定の順序で物を返すことが保証されていないため、メモリ内リストを使用してテストするときに渡すものを書き込むことができます( linq-to-entities)ですが、(linq-to-sql)を使用すると、uat / live環境で失敗します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.