web-dev-qa-db-ja.com

データベーストランザクションをモックしますか?

私は、インシデントとインシデントの詳細-親子関係を持つテーブルのペアを持っています。これらのテーブルの両方からの情報を含むビューモデルがあります。そして、両方のテーブルを更新する必要があるビューモデルのインスタンスを渡されるビジネスレイヤーメソッドがあります。

したがって、メソッドでは、EF6の新しいトランザクションメカニズムを使用しています。

using (var transaction = this.db.Database.BeginTransaction())
{
    try
    {
        // various database stuff
        this.db.SaveChanges();
        // more database stuff
        this.db.SaveChanges();
        // yet more database stuff
        this.db.SaveChanges();

        transaction.Commit();
    }
    catch (Exception ex)
    {
        transaction.Rollback();
        this.logger.logException(ex, "Exception caught in transaction, rolling back");
        throw;
    }
}

そして、私の問題です。これをどのようにテストしますか?

私はMoqでMicrosoftのユニットテストフレームワークを使用しており、DBContextsとDbSet <>のモックアップに問題はありませんでしたが、トランザクションに関する問題を回避する方法がわかりません。

トランザクションをモックしないと、InvalidOperationExceptionが発生します。

「xxxという名前の接続文字列がアプリケーション構成ファイルで見つかりませんでした。」

これは完全に理にかなっています。アプリケーションの構成ファイルやデータベースはありません。

しかし、BeginTransaction()をモックしようとすると、初期化エラーが発生します:NotSupportedException:

「非仮想メンバーの無効なセットアップ:m => m.Database.BeginTransaction」。

そして、私は雑草を追いかけて、.NETメソッドの逆コンパイルを調べ、使用可能なインターフェイス、またはモックオブジェクトを注入できる何かから派生したクラスを特定しようとしました。

MSのトランザクションコードを単体テストするつもりはありません。各テーブルの適切なレコードに適切な変更が加えられていることを確認したいだけです。しかし、現状では、これはテスト不可能であり、トランザクションを使用するメソッドはテスト不可能であるように見えます。そしてそれはただの苦痛です。

私は周りをグーグル検索しましたが、何も使用できませんでした。誰かがこの問題に遭遇しましたか?誰でもどのように進めるかについてのアイデアがありますか?

25
Jeff Dege

この種のものをテストすることは常に複雑ですが、最初に、ビジネスロジックを単体テストするか、アプリケーションを統合テストするかを自問する必要があります。

ロジックを単体テストしたい場合は、基本的にエンティティフレームワークをモックしようとすべきではありません。EFをテストしたくないので、コードをテストしたいだけですよね。そのためには、データアクセスオブジェクトをモックし、ビジネスロジックの単体テストのみを行います。

ただし、データアクセスレイヤーが機能するかどうかをテストする場合は、コードが、実装したすべてのCRUD操作を処理できる場合は、実際のデータベースに対して統合テストを行う必要があります。この場合、データアクセスオブジェクト(EF)をモックしないでください。たとえば、テストデータベースまたはsql-express localDBに対してテストを実行するだけです。

13
MichaC

コンテキストとトランザクションをインターフェイスでラップしてから、いくつかのプロバイダークラスでインターフェイスを実装できます。

public interface IDbContextProvider
{
    YourContext Context { get; set; }
    DbContextTransaction DbTransaction { get; set; }
    void Commit();
    void Rollback();
    void BeginTransaction();
    void SaveChanges();
}

そしてそれを実装します:

public class EfContextProvider : IDbContextProvider
{
    public EfContextProvider(YourContext context)
    {
        Context = context;
    }
    public YourContext Context { set; get; }
    public DbContextTransaction DbTransaction { set; get; }

    public void Commit()
    {
        DbTransaction.Commit();
    }

    public void Rollback()
    {
        DbTransaction.Rollback();
    }

    public void BeginTransaction()
    {
        DbTransaction=Context.Database.BeginTransaction();
    }

    public void SaveChanges()
    {
        Context.SaveChanges();
    }
}

だから今あなたのクラスにIDbContextProvider依存関係を与え、それで動作します(内部にコンテキストも持っています)。多分、usingブロックを_contextProvider.BeginTransaction();に置き換えます。そして、_contextProvider.Commit();または_contextProvider.Rollback();

11
Ivaylo Pashov

私はそれを理解するために数時間を費やしましたが、ラッパーや新しいクラスなしでMS Fakesによって直接実行できると思いました。

次の3つの手順を実行する必要があります。

  1. DbContextTransactionのshimオブジェクトを作成し、そのCommitメソッドとRollbackメソッドを迂回して何もしません。
  2. データベースのシムオブジェクトを作成します。そして、BeginTransactionメソッドを迂回して、手順1で作成したDbContextTransactionシムオブジェクトを返します。
  3. すべてのインスタンスのDbContext.Databaseプロパティを迂回して、手順2で作成されたデータベースシムオブジェクトを返します。

そして、すべて。

    static void SetupDBTransaction()
    {
        System.Data.Entity.Fakes.ShimDbContextTransaction transaction = new System.Data.Entity.Fakes.ShimDbContextTransaction();
        transaction.Commit = () => { };
        transaction.Rollback = () => { };

        System.Data.Entity.Fakes.ShimDatabase database = new System.Data.Entity.Fakes.ShimDatabase();
        database.BeginTransactionIsolationLevel = (isolationLevel) =>{return transaction.Instance;};

        System.Data.Entity.Fakes.ShimDbContext.AllInstances.DatabaseGet = (@this) => { return database.Instance; };
    }
3
Kaboo

EFクラスをPOCOクラスとして表し、データベースアダプタークラス内のすべてのデータベース相互作用を分離できます。これらのアダプタークラスには、ビジネスロジックをテストするときにモックできるインターフェイスがあります。

アダプタークラスのデータベース操作は、実際のデータベース接続でテストできますが、単体テスト用の専用データベースと接続文字列を使用できます。

では、トランザクションにラップされたビジネスコードをテストするのはどうでしょうか。

ビジネスコードをデータベースアダプターから分離するには、モックできるEFトランザクションスコープのインターフェイスを作成する必要があります。

私は以前、EFではなく次のようなデザインで作業しましたが、同様のPOCOラッピングを使用しました(構文またはサニティチェックではなく、疑似C#で)。

interface IDatabaseAdapter 
{
    ITransactionScope CreateTransactionScope();
}

interface ITransactionScope : IDisposable
{
    void Commit();
    void Rollback();        
}

class EntityFrameworkTransactionScope : ITransactionScope
{
    private DbContextTransaction entityTransaction;
    EntityFrameworkTransactionScope(DbContextTransaction entityTransaction)
    {
        this.entityTransaction = entityTransaction;
    }

    public Commit() { entityTransaction.Commit(); }
    public Rollback() { entityTransaction.Rollback(); }
    public Dispose() { entityTransaction.Dispose(); }

}

class EntityFrameworkAdapterBase : IDatabaseAdapter
{
   private Database database;
   protected EntityFrameworkAdapterBase(Database database)
   {
       this.database = database;
   }

   public ITransactionScope CreateTransactionScope()
   {
       return new EntityFrameworkTransactionScope(database.BeginTransaction());
   }
}

interface IIncidentDatabaseAdapter : IDatabaseAdapter
{
    SaveIncident(Incident incident);
}

public EntityIncidentDatabaseAdapter : EntityFrameworkAdapterBase, IIncidentDatabaseAdapter
{
    EntityIncidentDatabaseAdapter(Database database) : base(database) {}

    SaveIncident(Incident incident)
    {
         // code for saving the incident
    }
}

上記の設計により、ビジネスロジックやトランザクションを気にすることなくエンティティフレームワーク操作の単体テストを作成し、データベース障害をモックしてMOQなどを使用してロールバックが実際に呼び出されていることを確認できるビジネスロジックの単体テストを作成できます。 ITransactionScopeモック。上記のようなものを使用すると、ビジネスロジックのどの段階でも、考えられるほとんどすべてのトランザクションの失敗をカバーできるはずです。

もちろん、ユニットテストをいくつかの優れた統合テストで補足する必要があります。トランザクションがトリッキーになる可能性があり、特に、同時に使用するとトリッキーなデッドロックが発生し、モックテストでキャッチするのが難しいためです。

1
Holstebroe

私たちは、このコードとともにivaylo-pashovのソリューションを実装しました。

//Dependency Injection
public static void RegisterTypes(IUnityContainer container)
        {
            // Register manager mappings.
            container.RegisterType<IDatabaseContextProvider, EntityContextProvider>(new PerResolveLifetimeManager());
        }
    }

//Test Setup
        /// <summary>
        ///     Mocked <see cref="IrdEntities" /> context to be used in testing.
        /// </summary>
        private Mock<CCMSEntities> _irdContextMock;
        /// <summary>
        ///     Mocked <see cref="IDatabaseContextProvider" /> context to be used in testing.
        /// </summary>
        private Mock<IDatabaseContextProvider> _EntityContextProvider;
...

            _irdContextMock = new Mock<CCMSEntities>();
            _irdContextMock.Setup(m => m.Outbreaks).Returns(new Mock<DbSet<Outbreak>>().SetupData(_outbreakData).Object);
            _irdContextMock.Setup(m => m.FDI_Number_Counter).Returns(new Mock<DbSet<FDI_Number_Counter>>().SetupData(new List<FDI_Number_Counter>()).Object);

            _EntityContextProvider = new Mock<IDatabaseContextProvider>();
            _EntityContextProvider.Setup(m => m.Context).Returns(_irdContextMock.Object);

            _irdOutbreakRepository = new IrdOutbreakRepository(_EntityContextProvider.Object, _loggerMock.Object);

// Usage in the Class being tested:
//Constructor
        public IrdOutbreakRepository(IDatabaseContextProvider entityContextProvider, ILogger logger)
        {
            _entityContextProvider = entityContextProvider;
            _irdContext = entityContextProvider.Context;
            _logger = logger;
        }

        /// <summary>
        ///     The wrapper for the Entity Framework context and transaction.
        /// </summary>
        private readonly IDatabaseContextProvider _entityContextProvider;

        // The usage of a transaction that automatically gets mocked because the return type is void.
        _entityContextProvider.BeginTransaction();
...
0
Orion