2

私は1つのことについて非常に多く(何十もの投稿)を読みました:

Entity Framework コードを含むビジネス ロジック コードを単体テストする方法。

私は3つのレイヤーを持つWCFサービスを持っています:

  • サービス層
  • ビジネスロジックレイヤー
  • データ アクセス層

私のビジネス ロジックは、すべてのデータベース操作にDbContextを使用します。私のエンティティはすべてPOCOになりました(以前はObjectContextでしたが、変更しました)。

Ladislav Mrnka の回答herehereを読んで、 DbContextをモック \ 偽造してはならない 理由について説明しました。

彼は次 のように述べています。

そして: 「確かに、あなたのアプローチはいくつかのケースで機能しますが、ユニットテスト戦略はすべてのケースで機能する必要があります-それを機能させるには、テストされたメソッドからEFとIQueryableを完全に移動する必要があります。」

私の質問は - どうやってこれを達成するのですか???

public class TaskManager
{
    public void UpdateTaskStatus(
        Guid loggedInUserId,
        Guid clientId,
        Guid taskId,
        Guid chosenOptionId,
        Boolean isTaskCompleted,
        String notes,
        Byte[] rowVersion
    )
    {
        using (TransactionScope ts = new TransactionScope())
        {
            using (CloseDBEntities entities = new CloseDBEntities())
            {
                User currentUser = entities.Users.SingleOrDefault(us => us.Id == loggedInUserId);
                if (currentUser == null)
                    throw new Exception("Logged user does not exist in the system.");

                // Locate the task that is attached to this client
                ClientTaskStatus taskStatus = entities.ClientTaskStatuses.SingleOrDefault(p => p.TaskId == taskId && p.Visit.ClientId == clientId);
                if (taskStatus == null)
                    throw new Exception("Could not find this task for the client in the database.");

                if (taskStatus.Visit.CustomerRepId.HasValue == false)
                    throw new Exception("No customer rep is assigned to the client yet.");
                TaskOption option = entities.TaskOptions.SingleOrDefault(op => op.Id == optionId);
                if (option == null)
                    throw new Exception("The chosen option was not found in the database.");

                if (taskStatus.RowVersion != rowVersion)
                    throw new Exception("The task was updated by someone else. Please refresh the information and try again.");

                taskStatus.ChosenOptionId = optionId;
                taskStatus.IsCompleted = isTaskCompleted;
                taskStatus.Notes = notes;

                // Save changes to database
                entities.SaveChanges();
            }

            // Complete the transaction scope
            ts.Complete();
        }
    }
}

添付のコードには、私のビジネス ロジックからの関数のデモがあります。関数には、データベースへの「トリップ」がいくつかあります。この関数から EF コードを別のアセンブリに取り除く方法を正確に理解していないため、この関数の単体テスト(EF データの代わりにいくつかの偽のデータを挿入することによって) と、アセンブリの統合テストを行うことができます。 「EF関数」が含まれています。

Ladislav または他の誰かが助けてくれますか?

[編集]

これは私のビジネス ロジックのコードの別の例です。テスト済みのメソッドから「EF と IQueryable コードを移動」する方法がわかりません。

public List<UserDto> GetUsersByFilters(
    String ssn, 
    List<Guid> orderIds, 
    List<MaritalStatusEnum> maritalStatuses, 
    String name, 
    int age
)
{
    using (MyProjEntities entities = new MyProjEntities())
    {
        IQueryable<User> users = entities.Users;

        // Filter By SSN (check if the user's ssn matches)
        if (String.IsNullOrEmusy(ssn) == false)
            users = users.Where(us => us.SSN == ssn);

        // Filter By Orders (check fi the user has all the orders in the list)
        if (orderIds != null)
            users = users.Where(us => UserContainsAllOrders(us, orderIds));

        // Filter By Marital Status (check if the user has a marital status that is in the filter list)
        if (maritalStatuses != null)
            users = users.Where(pt => maritalStatuses.Contains((MaritalStatusEnum)us.MaritalStatus));

        // Filter By Name (check if the user's name matches)
        if (String.IsNullOrEmusy(name) == false)
            users = users.Where(us => us.name == name);

        // Filter By Age (check if the user's age matches)
        if (age > 0)
            users = users.Where(us => us.Age == age);


        return users.ToList();
    }
}

private   Boolean   UserContainsAllOrders(User user, List<Guid> orderIds)
{
    return orderIds.All(orderId => user.Orders.Any(order => order.Id == orderId));
}
4

2 に答える 2

5

クラスを単体テストする場合は、 Repository 設計パターンを採用し、 UserRepository や ClientTaskStatusRepository などのリポジトリをこのクラスに挿入する必要があります。次に、オブジェクトを構築する代わりに、これらのリポジトリを使用してそれらのメソッドを呼び出します。次に例を示します。TaskManagerCloseDBEntities

User currentUser = userRepository.GetUser(loggedInUserId);
ClientTaskStatus taskStatus = 
    clientTaskStatusRepository.GetTaskStatus(taskId, clientId);

クラスの統合テストを行いたい場合TaskManager、解決策ははるかに簡単です。CloseDBEntitiesテスト データベースを指す接続文字列を使用してオブジェクトを初期化するだけで済みます。これを実現する方法の 1 つは、CloseDBEntitiesオブジェクトをTaskManagerクラスに注入することです。

また、各統合テストを実行する前にテスト データベースを再作成し、いくつかのテスト データを入力する必要があります。これは、 Database Initializerを使用して実現できます。

于 2012-06-08T10:08:18.903 に答える
4

ここにはいくつかの誤解があります。

最初:リポジトリパターン。これは、単体テスト用のDbSetのファサードだけではありません。リポジトリは、ドメイン駆動設計のAggregateおよびAggreateRootの概念に強く関連するパターンです。。アグリゲートは、相互に一貫性を保つ必要がある関連エンティティのセットです。私は、外部キーの有効性だけでなく、ビジネスの一貫性を意味します。例:2回注文した顧客は、5%の割引を受ける必要があります。したがって、顧客エンティティに関連する注文エンティティの数と顧客エンティティの割引プロパティの間の一貫性を何らかの方法で管理する必要があります。これを担当するノードは集約ルートです。また、アグリゲートの外部から直接アクセスできる必要がある唯一のノードです。また、リポジトリは、一部の(おそらく永続的な)ストレージから集約ルートを取得するためのユーティリティです。

典型的なユースケースは、UoW / Transaction / DbContext / WhateverYouNameItを作成し、リポジトリから1つの集約ルートエンティティを取得するか、そのメソッドを呼び出すか、ルートから移動して他のエンティティにアクセスすることです。Commit/ SaveChanges/Whatever。ほら、それがyurサンプルとどれだけ違うか。

2番目:ビジネスロジック。すでに1つの例を示しました。2つの注文をした顧客は5%の割引を受ける必要があります。逆に、2番目のコードサンプルはビジネスロジックではありません。それは単なるクエリです。このコードの責任は、ストレージからデータを取得することです。このような場合、その背後にあるストレージテクノロジー重要です。したがって、ストレージとの対話がこの機能の唯一の目的である場合、ストレージが問題ではないふりをするのではなく、ここで統合テストをお勧めします。

また、すでに提案されているクエリオブジェクトにそれをカプセル化します。次に、そのようなクエリオブジェクトをモックすることができます。その背後にあるDbContextだけではありません。全体のQO。

最初のコードサンプルは、おそらくいくつかのビジネスロジックを含んでいるため、少し優れていますが、それを特定するのは困難です。Wichは私たちを3番目の問題に導きます。

第三に:貧血ドメインモデル。あなたのドメインはあまりオブジェクト指向に見えません。いくつかのダムエンティティとトランザクションスクリプトがあります。7つのパラメータで!それは純粋な手続き型プログラミングです。

さらに、UpdateTaskStatusのユースケースでは、集約ルートとは何ですか?あなたがそれに答える前に、最初に最も重要な質問:あなたは正確に何をしたいですか?それは...うーん...彼が訪問されたときに行われたユーザーの現在のタスクをマークしていますか?それよりも、顧客エンティティ内にVisit()メソッドがあるはずですか?そして、このメソッドは次のようなものでなければなりません。CurrentTaskStatus.IsCompleted= true?それは単なるランダムな推測でした。私が逃した場合、それは明らかに別の問題を示しています。ドメインモデルは、ユビキタス言語を使用する必要があります。これは、プログラマーとビジネスに共通の言語です。あなたのコードには、一般的な言語が与えるような表現力がありません。7つのパラメーターを持つUpdateTaskStatusで何が起こっているのかわかりません。

エンティティでビジネスオペレーションを実行するための適切な表現方法を配置すると、エンティティは永続性を無視したままにする必要があるため、DbContextをまったく使用しないように強制されます。その後、モックの問題はなくなります。永続性を気にすることなく、純粋なビジネスロジックをテストできます。

つまり、最後の言葉は、最初にモデルを再検討することです。最初にユビキタス言語を使用して、APIを表現力豊かにします。

PS:私を権威として扱わないでください。DDDを学び始めたばかりなので、完全に間違っている可能性があります。

于 2012-06-18T16:24:52.003 に答える