7

ビジネスロジックでEFをモック/偽造しようとすることの長所と短所についての投稿を何十も読んだことがあります。何をすべきかはまだ決めていませんが、1 つわかっているのは、クエリをビジネス ロジックから分離する必要があるということです。この投稿で、Ladislav が 2 つの良い方法があると答えているのを見ました。

  1. カスタム拡張メソッド、クエリ ビュー、マップされたデータベース ビュー、またはカスタム定義クエリを使用して、再利用可能なパーツを定義します。
  2. すべてのクエリを別のクラスのメソッドとして公開します。メソッドは IQueryable を公開してはならず、パラメーターとして Expression を受け入れてはなりません。つまり、クエリ ロジック全体をメソッドでラップする必要があります。しかし、これにより、関連するメソッドをカバーするクラスがリポジトリのようになります (モックまたは偽造できる唯一のもの)。この実装は、ストアド プロシージャで使用される実装に近いものです。
  1. どの方法が良いと思いますか?
  2. クエリを独自の場所に配置することの欠点ありますか? (おそらく、EF などから一部の機能が失われる可能性があります)
  3. 次のような最も単純なクエリでもカプセル化する必要がありますか?

    using (MyDbContext entities = new MyDbContext)
    {
        User user = entities.Users.Find(userId);  // ENCAPSULATE THIS ?
    
        // Some BL Code here
    }
    
4

1 に答える 1

7

ですから、あなたの主なポイントはコードのテスト容易性だと思いますよね? このような場合、テストしたいメソッドの責任を数えることから始め、単一の責任パターンを使用してコードをリファクタリングする必要があります。

サンプル コードには、少なくとも 3 つの役割があります。

  • オブジェクトの作成は責任です。コンテキストはオブジェクトです。さらに、単体テストで使用したくないオブジェクトであるため、その作成を別の場所に移動する必要があります。
  • クエリの実行は責任です。さらに、単体テストでは避けたい責任です。
  • いくつかのビジネスロジックを実行することは責任です

テストを簡素化するには、コードをリファクタリングし、それらの責任を別のメソッドに分割する必要があります。

public class MyBLClass()
{
    public void MyBLMethod(int userId)
    {
        using (IMyContext entities = GetContext())
        {
            User user = GetUserFromDb(entities, userId);

            // Some BL Code here
        }
    }

    protected virtual IMyContext GetContext()
    {
        return new MyDbContext();
    }

    protected virtual User GetUserFromDb(IMyDbContext entities, int userId)
    {
        return entities.Users.Find(userId);
    }
}

単体テストは、クラスと偽のコンテキスト ファクトリ メソッドとクエリ実行メソッドを継承し、EF に完全に依存することができるため、ビジネス ロジックの単体テストは簡単に行うことができます。

// NUnit unit test
[TestFixture]
public class MyBLClassTest : MyBLClass
{
    private class FakeContext : IMyContext
    {
        // Create just empty implementation of context interface
    }

    private User _testUser;

    [Test]
    public void MyBLMethod_DoSomething() 
    {
        // Test setup
        int id = 10;
        _testUser = new User 
            { 
                Id = id, 
                // rest is your expected test data - that  is what faking is about
                // faked method returns simply data your test method expects
            };

        // Execution of method under test
        MyBLMethod(id);

        // Test validation
        // Assert something you expect to happen on _testUser instance 
        // inside MyBLMethod
    }

    protected override IMyContext GetContext()
    {
        return new FakeContext();
    }

    protected override User GetUserFromDb(IMyContext context, int userId)
    {
        return _testUser.Id == userId ? _testUser : null;
    }
}

メソッドを追加し、アプリケーションが成長するにつれて、これらのクエリ実行メソッドとコンテキスト ファクトリ メソッドをリファクタリングしてクラスを分離し、クラスに対する単一の責任に従うことになります。コンテキスト ファクトリと、いくつかのクエリ プロバイダまたは場合によってはリポジトリを取得します (ただし、リポジトリは、そのメソッドのいずれかでパラメーターとして返されIQueryableたり取得されたりすることはありません)。Expressionこれにより、コンテキストの作成と最も一般的に使用されるクエリが 1 つの中央の場所で 1 回だけ定義されるという DRY 原則に従うこともできます。

したがって、最後に次のようなものを作成できます。

public class MyBLClass()
{
    private IContextFactory _contextFactory;
    private IUserQueryProvider _userProvider;

    public MyBLClass(IContextFactory contextFactory, IUserQueryProvider userProvider)
    {
        _contextFactory = contextFactory;
        _userProvider = userProvider;
    }

    public void MyBLMethod(int userId)
    {
        using (IMyContext entities = _contextFactory.GetContext())
        {
            User user = _userProvider.GetSingle(entities, userId);

            // Some BL Code here
        }
    }
}

これらのインターフェースは次のようになります。

public interface IContextFactory 
{
    IMyContext GetContext();
}

public class MyContextFactory : IContextFactory
{
    public IMyContext GetContext()
    {
        // Here belongs any logic necessary to create context
        // If you for example want to cache context per HTTP request
        // you can implement logic here.
        return new MyDbContext();
    } 
}

public interface IUserQueryProvider
{
    User GetUser(int userId);

    // Any other reusable queries for user entities
    // Non of queries returns IQueryable or accepts Expression as parameter
    // For example: IEnumerable<User> GetActiveUsers();
}

public class MyUserQueryProvider : IUserQueryProvider
{
    public User GetUser(IMyContext context, int userId)
    {
        return context.Users.Find(userId);
    }

    // Implementation of other queries

    // Only inside query implementations you can use extension methods on IQueryable
}

テストは、コンテキスト ファクトリとクエリ プロバイダーにフェイクのみを使用するようになりました。

// NUnit + Moq unit test
[TestFixture]
public class MyBLClassTest
{
    private class FakeContext : IMyContext
    {
        // Create just empty implementation of context interface 
    }

    [Test]
    public void MyBLMethod_DoSomething() 
    {
        // Test setup
        int id = 10;
        var user = new User 
            { 
                Id = id, 
                // rest is your expected test data - that  is what faking is about
                // faked method returns simply data your test method expects
            };

        var contextFactory = new Mock<IContextFactory>();
        contextFactory.Setup(f => f.GetContext()).Returns(new FakeContext());

        var queryProvider = new Mock<IUserQueryProvider>();
        queryProvider.Setup(f => f.GetUser(It.IsAny<IContextFactory>(), id)).Returns(user);

        // Execution of method under test
        var myBLClass = new MyBLClass(contextFactory.Object, queryProvider.Object);
        myBLClass.MyBLMethod(id);

        // Test validation
        // Assert something you expect to happen on user instance 
        // inside MyBLMethod
    }
}

ビジネスクラスに注入する前に、コンテキストへの参照をコンストラクターに渡す必要があるリポジトリの場合は、少し異なります。あなたのビジネス クラスは、他のクラスでは決して使用されないいくつかのクエリを定義することができます。これらのクエリは、おそらくそのロジックの一部です。拡張メソッドを使用して、クエリの再利用可能な部分を定義することもできますが、ユニット テストするコア ビジネス ロジックの外部 (クエリ実行メソッドまたはクエリ プロバイダー/リポジトリ) で拡張メソッドを常に使用する必要があります。これにより、クエリ プロバイダーまたはクエリ実行メソッドを簡単に偽装できます。

以前の質問を見て、そのトピックに関するブログ投稿を書くことを考えましたが、EF でのテストに関する私の意見の核心はこの回答にあります。

編集:

リポジトリは、元の質問とは関係のない別のトピックです。特定のリポジトリは引き続き有効なパターンです。私たちはレポジトリに反対しているわけではありません。一般的なレポジトリには反対です。なぜなら、それらは追加機能を提供せず、問題を解決しないからです。

問題は、リポジトリだけでは何も解決しないことです。適切な抽象化を形成するために一緒に使用する必要がある 3 つのパターンがあります: リポジトリ、作業単位、および仕様です。3 つすべてが既に EF で利用可能です: DbSet / ObjectSet はリポジトリとして、DbContext / ObjectContext は作業単位として、Linq to Entities は仕様として。どこでも言及されている汎用リポジトリのカスタム実装の主な問題は、リポジトリと作業単位のみをカスタム実装に置き換えますが、元の仕様に依存していることです => 抽象化が不完全であり、偽のリポジトリが偽造されたセット/コンテキスト。

私のクエリ プロバイダーの主な欠点は、実行する必要があるクエリの明示的な方法です。リポジトリの場合、そのようなメソッドはありません。仕様を受け入れるメソッドはほとんどありません (ただし、これらの仕様は DRY 原則で定義する必要があります)。クエリのフィルタリング条件や順序付けなどを構築します。

public interface IUserRepository
{
    User Find(int userId);
    IEnumerable<User> FindAll(ISpecification spec);
}

このトピックの議論は、この質問の範囲をはるかに超えているため、自己学習が必要です。

ところで。モックと偽造には異なる目的があります。依存関係のメソッドからテストデータを取得する必要がある場合は呼び出しを偽造し、依存関係のメソッドが予想される引数で呼び出されたことをアサートする必要がある場合は呼び出しを偽造します。

于 2012-06-10T11:16:22.620 に答える