Unit Testing sebuah kelas yang meminta data dari berbagai sumber

Aug 17 2020

Konteks

Saya sedang mengerjakan proyek yang mengambil data dari AWS menggunakan berbagai AWS SDK untuk .NET. Contoh khusus ini berhubungan dengan AWSSDK.IdentityManagementSDK

Tujuannya adalah untuk meminta informasi dari IAmazonIdentityManagementServicedan memetakannya ke model yang berguna untuk domain bisnis tempat saya bekerja

Saya telah ditugaskan untuk menulis tes unit untuk IamServicekelas.

Masalah

Dengan pengaturan Unit Test yang begitu bertele-tele, saya tidak bisa tidak berpikir metode I'm Unit Testing ( GetIamSummaryAsync) harus dibangun dengan buruk.

Saya telah mencari di Google untuk hal-hal seperti "Pola desain untuk memetakan beberapa sumber data ke objek tunggal", tetapi satu-satunya saran yang saya lihat diberikan adalah dengan menggunakan pola Adaptor atau Proxy. Saya tidak yakin bagaimana menerapkannya ke skenario ini

Pertanyaan

  • Adakah cara yang lebih baik IamServiceuntuk membuat kelas saya agar lebih mudah (lebih ringkas) untuk diuji?
  • Jika pola Adaptor atau Proxy sesuai untuk jenis skenario ini, bagaimana penerapannya?
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;
    }
}

Dimana kelas IamSummaryModeltersebut didefinisikan sebagai:

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; }
}

Masalah yang saya hadapi adalah Tes Unit saya berubah menjadi banyak kode di bagian Assemble. Saya harus mengejek setiap panggilan yang saya lakukan ke setiap metode klien AWS SDK.

Contoh Uji Unit

[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);
}

Jawaban

2 Flater Aug 17 2020 at 17:55

Tidak ada yang salah dengan metode ini. Ini menarik banyak informasi, tetapi terkadang itu adalah sesuatu yang harus dilakukan (misalnya untuk pelaporan, atau menyiapkan transfer data yang besar).
Tidak dapat dipungkiri bahwa saat Anda mengejek sumber data Anda, semakin banyak sumber yang Anda miliki, semakin banyak Anda harus mengejek. Itu tidak mudah dihindari. Namun, Anda dapat mengevaluasi kembali pendekatan Anda yang membawa Anda ke sini.

1. Apakah data ini perlu digabungkan?

Pertanyaan pertama yang harus Anda tanyakan pada diri sendiri adalah apakah menggabungkan data ini perlu. Jika tidak, dan Anda dapat menyimpan data ini secara terpisah, maka itu cara yang bagus untuk menjaga basis kode Anda lebih sederhana dan lebih mudah untuk dipalsukan (dan dengan demikian diuji).
Jika data ini perlu digabungkan di beberapa titik, maka refactoring kelas Anda hanya menggeser data logika-menggabungkan ke tingkat lain, di mana sama pertanyaan unit-pengujian sekarang muncul: bagaimana mengejek di dalam itu lapisan? Merelokasi logika tidak memperbaikinya.

2. Apakah saya perlu menguji unit ini?

Kedua, Anda harus mempertanyakan apakah pengujian unit dijamin di sini. Meskipun tidak semua orang setuju (secara pribadi, saya ragu-ragu), ada argumen yang masuk akal untuk dibuat IamServiceagar unit tidak diuji karena ini bukan kelas logika domain, melainkan pembungkus / pemeta dari sumber daya eksternal .

Demikian pula, saya tidak akan menguji kelas konteks EntityFramework, kecuali jika mengandung logika bisnis kustom (misalnya bidang audit otomatis), karena logika bisnis itu perlu diuji. Sisa kelas lainnya hanyalah penerapan EF, yang tidak memerlukan pengujian.

Anda IamServicesaat ini tidak memiliki logika bisnis yang nyata, jadi argumen untuk tidak mengujinya cukup kuat, menurut saya. Argumen bahwa pemetaan IamSummaryModelobjek dihitung sebagai logika bisnis, bisa diperdebatkan. Saya tidak selalu menguji pemetaan sepele karena kode sepele tidak boleh diuji (catatan: meskipun saya yakin ini benar, saya sadar bahwa sangat mudah untuk menyalahgunakan label "sepele" pada kode yang sebenarnya tidak sepele. WASPADALAH)

3. Bagaimana cara meminimalkan upaya mengejek?

Jika Anda mencapai titik ini, Anda setuju bahwa menggabungkan data dan pengujian unit kelas Anda diperlukan. Itu secara logis menyimpulkan perlunya memalsukan semua sumber data ini saat menguji kelas itu. Sekarang telah menjadi fakta yang tak terhindarkan.

Tetapi itu tidak berarti Anda tidak dapat membuat hidup Anda lebih mudah dengan menggunakan kembali / menyederhanakan logika pengaturan. Biarkan kelas pengujian Anda mewarisi dari kelas dasar yang digunakan sebagai perlengkapan, atau mengimplementasikan properti yang berisi perlengkapan tersebut. Untuk jawaban ini, saya akan memilih rute warisan, tetapi keduanya berhasil.

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);
    }
}

Ini adalah implementasi yang sangat cepat dari perlengkapan semacam itu. Perlengkapan itu dapat melakukan sebagian besar kerja keras untuk Anda. Jika Anda memiliki lebih dari satu pengujian, yang menurut saya sangat banyak, hal ini akan sangat mengurangi kerumitan karena harus menyiapkannya untuk setiap pengujian individu.

Saat menyiapkan tiruan, Anda dapat mengandalkan nilai yang Anda pilih dan membuatnya dapat diakses melalui properti, yang kemudian dapat Anda gunakan kembali untuk logika pernyataan Anda. Sebagai contoh:

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()
        );
    }
}

Perhatikan bagaimana saya menyiapkan respons yang diejek secara spesifik, dan kemudian saya dapat menggunakan respons yang diolok-olok itu untuk membandingkannya dengan respons aktual yang diterima dari unit yang sedang diuji.

Jika Anda perlu menulis pengujian khusus untuk kebijakan tertentu, Anda dapat menambahkan parameter metode jika perlu, misalnya:

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);
     }
}

Anda mungkin akan memiliki logika pernyataan yang lebih kompleks saat menggunakan nilai tiruan kustom, tetapi ini hanyalah contoh dasar.


Catatan: seperti yang saya sebutkan dalam komentar untuk jawaban candied_orange, disarankan untuk tidak menggunakan antarmuka dari perpustakaan Anda di domain Anda (atau setidaknya sangat meminimalkannya), tetapi itu tidak terkait dengan inti pertanyaan Anda di sini jadi saya melewatkan poin itu.

4 candied_orange Aug 17 2020 at 15:03

Kelas yang mengetahui sumber data tidak dapat diuji unitnya. Ini hanya dapat diuji integrasi.

Kelas yang mengetahui struktur data dapat diuji unitnya. Yang Anda butuhkan adalah cara untuk menyediakan struktur data yang tidak memerlukan pengetahuan tentang sumber data.

Ini dapat berupa apa saja, mulai dari data uji berkode keras hingga dalam basis data memori. Tetapi jika Anda berbicara dengan sumber data Anda yang sebenarnya, Anda tidak melakukan pengujian unit .