複数のソースからのデータを要求するクラスのユニットテスト

Aug 17 2020

環境

さまざまなAWSSDK for.NETを使用してAWSからデータをプルするプロジェクトに取り組んでいます。この特定の例はAWSSDK.IdentityManagementSDKを扱います

目標は、IAmazonIdentityManagementService私が働いているビジネスドメインに役立つモデルから情報を照会し、それをモデルにマッピングすることです。

私はIamServiceクラスのユニットテストを書くことを任されてきました。

問題

ユニットテストの設定が非常に冗長であるため、私がユニットテスト(GetIamSummaryAsync)であるメソッドの構築が不十分であると思わざるを得ません。

「複数のデータソースを単一のオブジェクトにマッピングするためのデザインパターン」などをグーグルで検索しましたが、与えられた唯一のアドバイスは、アダプターまたはプロキシパターンを使用することです。このシナリオにそれらを適用する方法がわかりません

質問

  • IamServiceテストをより簡単に(より簡潔に)するためにクラスを構築するためのより良い方法はありますか?
  • アダプターまたはプロキシパターンがこのタイプのシナリオに適している場合、それらはどのように適用されますか?
public class IamService : IIamService
{
    IAmazonIdentityManagementService _iamClient;

    public IamService(IAmazonIdentityManagementService iamClient)
    {
        _iamClient = iamClient;
    }

    public async Task<IamSummaryModel> GetIamSummaryAsync()
    {
        var getAccountSummaryResponse           = await _iamClient.GetAccountSummaryAsync();
        var listCustomerManagedPoliciesResponse = await _iamClient.ListPoliciesAsync();
        var listGroupsResponse                  = await _iamClient.ListGroupsAsync();
        var listInstanceProfilesResponse        = await _iamClient.ListInstanceProfilesAsync();
        var listRolesResponse                   = await _iamClient.ListRolesAsync();
        var listServerCertificatesResponse      = await _iamClient.ListServerCertificatesAsync();
        var listUsersResponse                   = await _iamClient.ListUsersAsync();

        IamSummaryModel iamSummary = new IamSummaryModel();

        iamSummary.CustomerManagedPolicies.Count = listCustomerManagedPoliciesResponse.Policies.Count;
        iamSummary.CustomerManagedPolicies.DefaultQuota = getAccountSummaryResponse.SummaryMap["PoliciesQuota"];

        iamSummary.Groups.Count = listGroupsResponse.Groups.Count;
        iamSummary.Groups.DefaultQuota = getAccountSummaryResponse.SummaryMap["GroupsQuota"];

        iamSummary.InstanceProfiles.Count = listInstanceProfilesResponse.InstanceProfiles.Count;
        iamSummary.InstanceProfiles.DefaultQuota = getAccountSummaryResponse.SummaryMap["InstanceProfilesQuota"];

        iamSummary.Roles.Count = listRolesResponse.Roles.Count;
        iamSummary.Roles.DefaultQuota = getAccountSummaryResponse.SummaryMap["RolesQuota"];

        iamSummary.ServerCertificates.Count = listServerCertificatesResponse.ServerCertificateMetadataList.Count;
        iamSummary.ServerCertificates.DefaultQuota = getAccountSummaryResponse.SummaryMap["ServerCertificatesQuota"];

        iamSummary.Users.Count = listUsersResponse.Users.Count;
        iamSummary.Users.DefaultQuota = getAccountSummaryResponse.SummaryMap["UsersQuota"];

        return iamSummary;
    }
}

クラスIamSummaryModelが次のように定義されている場合:

public sealed class IamSummaryModel
{
    public ResourceSummaryModel CustomerManagedPolicies { get; set; } = new ResourceSummaryModel();
    public ResourceSummaryModel Groups { get; set; } = new ResourceSummaryModel();
    public ResourceSummaryModel InstanceProfiles { get; set; } = new ResourceSummaryModel();
    public ResourceSummaryModel Roles { get; set; } = new ResourceSummaryModel();
    public ResourceSummaryModel ServerCertificates { get; set; } = new ResourceSummaryModel();
    public ResourceSummaryModel Users { get; set; } = new ResourceSummaryModel();
}

public sealed class ResourceSummaryModel
{
    public int Count { get; set; }
    public int DefaultQuota { get; set; }
}

私が直面している問題は、ユニットテストがアセンブルセクションで大量のコードに変わることです。各AWSSDKクライアントメソッドに対して行うすべての呼び出しをモックする必要があります。

ユニットテストの例

[Fact]
public async Task GetIamSummaryAsync_CustomerManagerPolicies_MapToModel()
{
    // Arrange
    var iamClientStub = new Mock<IAmazonIdentityManagementService>();
    
    iamClientStub.Setup(iam => iam.ListPoliciesAsync(It.IsAny<CancellationToken>()))
        .Returns(Task.FromResult(
            new ListPoliciesResponse()
            {
                Policies = new List<ManagedPolicy>()
                {
                    new ManagedPolicy(),
                    new ManagedPolicy()
                }
            }
        ));

    // Lots of other mocks, one for each dependency
    
    var sut = new IamService(iamClientStub.Object);

    // Act
    var actual = await sut.GetIamSummaryAsync();

    // Assert
    Assert.Equal(2, actual.CustomerManagedPolicies.Count);
}

回答

2 Flater Aug 17 2020 at 17:55

この方法に問題はありません。それは多くの情報を引き出しますが、時にはそれをしなければならないことがあります(例えば、報告や大規模なデータ転送の準備のために)。
データソースをモックする場合、ソースが多ければ多いほど、モックを作成する必要があります。それは簡単には避けられません。ただし、ここに導いたアプローチを再評価することはできます。

1.このデータを組み合わせる必要がありますか?

自分自身に問う最初の質問は、このデータを組み合わせる必要があるかどうかです。そうではなく、このデータを分離しておくことができる場合、それはコードベースをよりシンプルでモック(したがってテスト)しやすくするための優れた方法です。
このデータをある時点で結合する必要がある場合、クラスをリファクタリングすると、データ結合ロジックが別のレベルにシフトするだけで、同じ単体テストの質問がポップアップします。そのレイヤーでモックインする方法は?ロジックを再配置しても修正されません。

2.これをユニットテストする必要がありますか?

次に、ここで単体テストが必要かどうかを質問する必要があります。誰もが同意するわけではありませんが(個人的には、私はフェンスにいます)、IamServiceドメインロジッククラスではなく、外部リソースのラッパー/マッパーであるため、単体テストを行わないようにするための合理的な議論があります。 。

同様に、カスタムビジネスロジック(自動監査フィールドなど)が含まれていない限り、EntityFrameworkコンテキストクラスもテストしません。そのビジネスロジックはテストが必要だからです。クラスの残りの部分はEFの実装であり、テストを保証するものではありません。

あなたIamServiceは現在、実際のビジネスロジックを欠いているので、ユニットテストを行わないという議論は非常に強いと思います。IamSummaryModelオブジェクトのマッピングがビジネスロジックとしてカウントされるという議論は、まあ、議論の余地があります。些細なコードをテストするべきではないため、常に些細なマッピングをテストするわけではありません(注:これは正しいと思いますが、実際には些細なコードでは「些細な」ラベルを誤用するのは非常に簡単です。注意)

3.モックの労力を最小限に抑えるにはどうすればよいですか?

この時点に達した場合は、データの組み合わせとクラスの単体テストの両方が必要であることに同意するものとします。これで、そのクラスをテストするときに、これらすべてのデータソースをモックする必要があると論理的に結論付けられます。それは今や避けられない事実になっています。

しかし、それはあなたがアレンジロジックを再利用/単純化することによってあなたの人生を楽にすることができないという意味ではありません。テストクラスに、フィクスチャとして使用される基本クラスを継承するか、そのフィクスチャを含むプロパティを実装させます。この回答では、継承ルートを選択しますが、どちらでも機能します。

public class IamServiceTestFixture
{
    protected IamService GetService()
    {
        var mockedAmazonService = GetMockedAmazonService();

        return new IamService(mockedAmazonService);
    }

    private IAmazonIdentityManagementService GetMockedAmazonService()
    {
        var iamClientStub = new Mock<IAmazonIdentityManagementService>();

        // Set up your mocks

        return iamClientStub;
    }
}

public class IamServiceTests : IamServiceTestFixture
{
    [Test]
    public void MyTest()
    {
        // Arrange
        var sut = GetService();

        // Act
        var actual = await sut.GetIamSummaryAsync();

        // Assert
        Assert.Equal(2, actual.CustomerManagedPolicies.Count);
    }
}

これは、そのようなフィクスチャの非常に迅速な実装です。そのフィクスチャはあなたのためにほとんどのレッグワークを行うことができます。複数のテストがある場合は、これにより、個々のテストごとにこれを設定する必要があるという複雑さが大幅に軽減されます。

モックを設定するときは、選択した値に依存し、プロパティを介してアクセスできるようにすることができます。これは、後でアサーションロジックに再利用できます。例えば:

public class IamServiceTestFixture
{
    protected ListPoliciesResponse ListPoliciesResponse { get; private set; }

    public IamServiceTestFixture()
    {
         this.ListPoliciesResponse = new ListPoliciesResponse()
         {
             Policies = new List<ManagedPolicy>()
             {
                 new ManagedPolicy(),
                 new ManagedPolicy()
             }
         }
    }

    protected IamService GetService()
    {
        var mockedAmazonService = GetMockedAmazonService();

        return new IamService(mockedAmazonService);
    }

    private IAmazonIdentityManagementService GetMockedAmazonService()
    {
        var iamClientStub = new Mock<IAmazonIdentityManagementService>();

        iamClientStub.Setup(iam => iam.ListPoliciesAsync(It.IsAny<CancellationToken>()))
            .Returns(Task.FromResult(this.ListPoliciesResponse));

        return iamClientStub;
    }
}

public class IamServiceTests : IamServiceTestFixture
{        
    [Test]
    public void MyTest()
    {
        // Arrange
        var sut = GetService();

        // Act
        var actual = await sut.GetIamSummaryAsync();

        // Assert
        Assert.Equal(
            this.ListPoliciesResponse.Policies.Count(), 
            actual.CustomerManagedPolicies.Count()
        );
    }
}

特定のモック応答を設定した方法に注目してください。その後、そのモック応答を使用して、テスト対象のユニットから受信した実際の応答と比較できます。

特定のポリシーに対して特定のテストを作成する必要がある場合は、必要に応じてメソッドパラメータを追加できます。例:

public class IamServiceTestFixture
{
    protected IamService GetService(IEnumerable<ManagedPolicy> policies)
    {
        var mockedAmazonService = GetMockedAmazonService(policies);

        return new IamService(mockedAmazonService);
    }

    private IAmazonIdentityManagementService GetMockedAmazonService(IEnumerable<ManagedPolicy> policies)
    {
        var iamClientStub = new Mock<IAmazonIdentityManagementService>();

        iamClientStub.Setup(iam => iam.ListPoliciesAsync(It.IsAny<CancellationToken>()))
            .Returns(Task.FromResult(new ListPoliciesResponse()
            {
                    Policies = policies
            }));

        return iamClientStub;
    }
}

public class IamServiceTests : IamServiceTestFixture
{
    [Test]
    public void MyTest()
    {
        var customPolicy = new ManagedPolicy();

        // Arrange
        var sut = GetService(new ManagedPolicy[] { customPolicy });

        // Act
        var actual = await sut.GetIamSummaryAsync();

        // Assert
        actual.CustomerManagedPolicies.Should().Contain(customPolicy);
     }
}

カスタムのモック値を使用する場合は、おそらくより複雑なアサーションロジックが必要になりますが、これは基本的な例にすぎません。


注:candied_orangeの回答へのコメントで述べたように、ドメイン内のライブラリのインターフェースを使用しない(または少なくとも大幅に最小化する)ことをお勧めしますが、それはここでの質問の核心とは無関係なので、その点はスキップします。

4 candied_orange Aug 17 2020 at 15:03

データソースを認識するクラスは、単体テストできません。統合テストのみが可能です。

データ構造を認識するクラスは、単体テストできます。必要なのは、データソースの知識を必要としないデータ構造を提供する方法です。

これらは、ハードコードされたテストデータからインメモリデータベースまで、あらゆるものになり得ます。ただし、実際のデータソースと話している場合は、単体テストではありません。