1. ホーム
  2. c#

[解決済み] Entity Framework 6のユニットテストはどうなっているのか、気になりますか?

2022-05-02 15:59:16

質問

私はユニットテストとTDD全般を始めたばかりです。以前にも手を出したことがありますが、今は自分のワークフローに追加して、より良いソフトウェアを書こうと決心しています。

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

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

ここで疑問なのですが、このようなことをする意味はあるのでしょうか?もしそうなら、IQueryableによって引き起こされたリークする抽象化、および以下のような多くの素晴らしい投稿を踏まえて、人々はどのようにそれを行っているのでしょうか? ラディスラフ・ムルンカ 特定のデータベースではなくインメモリ実装で作業する場合、Linqプロバイダの違いのためにユニットテストは簡単にはいかないというテーマについてです。

私がテストしたいコードは、かなり単純なようです。(これは私がやっていることを理解するためのダミーコードで、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 Contextをモック化し、以下のようなアプローチをとる。 ユニットテスト時のEFのモック化 あるいは、moq のようなインターフェース上で直接モッキングフレームワークを使用する - ユニットテストはパスするかもしれないが、必ずしもエンドツーエンドで動作するとは限らないという痛みを引き受け、それらを統合テストでバックアップする?
  2. のようなものを使うといいかもしれません。 努力 を使用してEFをモックする - 私は使ったことがないので、他の人が実際に使用しているかどうかはわかりません。
  3. EFを単純に呼び出すものはテストしない - つまり、EFを直接呼び出すサービスメソッド(getAllなど)は、ユニットテストではなく、統合テストだけなのでしょうか?

どなたか、Repoを使わずに実際にやって成功した方はいらっしゃいますか?

解決方法は?

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

しかし、その下のデータベースはあなたのものです。 EF/NHが正しく機能しているかどうかをテストする必要はないのです。マッピングや実装がデータベースと連動しているかどうかをテストする必要があるのです。私の意見では、これはシステムでテストできる最も重要な部分の1つです。

しかし、厳密に言えば、私たちは単体テストの領域を離れ、統合テストに移行しつつありますが、その原則は変わりません。

最初に必要なことは、BLLをEFやSQLから独立してテストできるように、DALをモック化することです。 これが単体テストです。 次に 統合テスト は、DALを証明するためのもので、私の意見では、これらは同じぐらい重要です。

いくつかありますね。

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

データベースのセットアップには主に2つのアプローチがあり、1つはUnitTest create 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つの基本クラスを持つことが多い。 SetupPerTestSingleSetup . この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());
      });
   }
}

まとめると、何をテストしようとしているかに応じて、どちらのアプローチも有効です。