Birden çok kaynaktan veri isteyen bir sınıfı test etme

Aug 17 2020

Bağlam

.NET için çeşitli AWS SDK'larını kullanarak AWS'den veri çeken bir proje üzerinde çalışıyorum. Bu özel örnek, AWSSDK.IdentityManagementSDK ile ilgilidir

Amaç, IAmazonIdentityManagementServiceçalıştığım iş alanı için yararlı olan bir modelden bilgi sorgulamak ve bu modelle eşleştirmek

IamServiceSınıf için birim testleri yazmakla görevlendirildim .

Sorun

Unit Test kurulumları bu kadar ayrıntılı olduğundan, yardım edemem ama Unit Testing ( GetIamSummaryAsync) yöntemimin kötü yapılandırılması gerektiğini düşünüyorum.

"Birden çok veri kaynağını tek nesnelere eşlemek için tasarım modelleri" gibi şeyler için etrafta dolaştım, ancak gördüğüm tek tavsiye Bağdaştırıcı veya Proxy modellerini kullanmaktır. Bunları bu senaryoya nasıl uygulayacağımdan emin değilim

Soru

  • IamServiceSınıfımı test etmeyi kolaylaştırmak (daha kısa ve öz) yapmak için daha iyi bir yol var mı ?
  • Bağdaştırıcı veya Proxy modelleri bu tür bir senaryo için uygunsa, nasıl uygulanır?
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;
    }
}

Sınıf IamSummaryModelşu şekilde tanımlanır:

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

Karşılaştığım sorun, Birim Testlerimin Montaj bölümünde bir kod kütlesine dönüşmesidir. Her AWS SDK istemci yöntemine yaptığım her çağrıyla dalga geçmem gerekiyor.

Örnek Birim Testi

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

Yanıtlar

2 Flater Aug 17 2020 at 17:55

Bu yöntemde yanlış bir şey yok. Çok fazla bilgi çekiyor, ancak bazen yapılması gereken bir şey bu (örneğin raporlama veya büyük bir veri aktarımı hazırlamak için).
Veri kaynağınızla dalga geçtiğinizde, ne kadar çok kaynağa sahipseniz, o kadar çok dalga geçmeniz kaçınılmazdır. Bu kolayca önlenemez. Ancak sizi buraya getiren yaklaşımınızı yeniden değerlendirebilirsiniz.

1. Bu verilerin birleştirilmesi gerekiyor mu?

Kendinize sormanız gereken ilk soru, bu verileri birleştirmenin gerekli olup olmadığıdır. Değilse ve bu verileri ayrı tutabilirseniz, bu, kod tabanınızı daha basit ve alay etmeyi (ve dolayısıyla test etmeyi) daha kolay tutmanın harika bir yoludur.
Bu verilerin bir noktada birleştirilmesi gerekiyorsa, sınıfınızı yeniden düzenlemek, veri birleştirme mantığını başka bir düzeye kaydırır, şimdi aynı birim testi sorusu ortaya çıkar: bu katmanda nasıl alay edilir ? Mantığın yerini değiştirmek onu düzeltmez.

2. Bunu birim test etmem gerekiyor mu?

İkinci olarak, burada birim testinin garanti edilip edilmediğini sorgulamalısınız. Herkes kabul etmese de (şahsen, çitin üzerindeyim), IamServicebir etki alanı mantık sınıfı olmadığı için birim test edilmemesi için makul bir argüman var, bunun yerine harici bir kaynağın sarmalayıcısı / eşleyicisi .

Benzer şekilde, bir EntityFramework bağlam sınıfını da, özel iş mantığı (örn. Otomatik denetim alanları) içermediği sürece test etmem , çünkü bu iş mantığının test edilmesi gerekir. Sınıfın geri kalanı, yalnızca EF'nin uygulanmasıdır, bu da test etmeyi gerektirmez.

Sizin IamServiceşu anda gerçek bir iş mantığından yoksundur, bu yüzden bence onu test etmeme argümanı oldukça güçlüdür. IamSummaryModelNesnenin haritalanmasının iş mantığı olarak sayıldığı argümanı tartışılabilir. Önemsiz kodun test edilmemesi gerektiğinden her zaman önemsiz eşlemeleri test etmiyorum (not: Bunun doğru olduğuna inanıyorum, ancak aslında önemsiz olmayan kod üzerindeki "önemsiz" etiketi kötüye kullanmanın çok kolay olduğunun farkındayım. DİKKAT)

3. Alay etme çabasını nasıl en aza indirebilirim?

Bu noktaya ulaştıysanız, hem verileri birleştirmenin hem de sınıfınızın birim testinin gerekli olduğunu kabul edersiniz. Bu mantıksal olarak, bu sınıfı test ederken tüm bu veri kaynaklarıyla dalga geçme ihtiyacı ile sonuçlanır. Artık kaçınılmaz bir gerçek haline geldi.

Ancak bu, düzenleme mantığını yeniden kullanarak / basitleştirerek hayatınızı kolaylaştıramayacağınız anlamına gelmez. Test sınıfınızın bir fikstür olarak kullanılan bir temel sınıftan miras almasına veya söz konusu fikstürü içeren bir özelliği uygulamasına izin verin. Bu cevap için miras yolunu seçeceğim, ancak ikisi de işe yarıyor.

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

Bu, böyle bir fikstürün çok hızlı bir uygulamasıdır. Bu fikstür, ayak işlerinin çoğunu sizin için yapabilir. Birden fazla testiniz varsa, ki bunu yapacağınızı çokça varsayıyorum, bu, her bir test için bunu ayarlamak zorunda kalmanın karmaşıklığını önemli ölçüde azaltacaktır.

Taklidi kurarken, seçtiğiniz değerlere güvenebilir ve özellikler aracılığıyla erişilebilir hale getirebilir ve daha sonra onaylama mantığınız için yeniden kullanabilirsiniz. Örneğin:

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

Belirli bir alaylı yanıtı nasıl oluşturduğuma dikkat edin ve daha sonra bu alay konusu yanıtı, test edilen ünitemden alınan gerçek yanıtla karşılaştırmak için kullanabilirim.

Belirli politikalar için belirli testler yazmanız gerekiyorsa, gerekli olduğu yerlerde yöntem parametreleri ekleyebilirsiniz, örneğin:

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

Özel alay edilen değerleri kullanırken muhtemelen daha karmaşık iddia mantığına sahip olacaksınız, ancak bu sadece basit bir örnek.


Not: candied_orange'ın cevabına yapılan yorumda bahsettiğim gibi, alanınızdaki kütüphanelerinizin arayüzlerini kullanmamanız (veya en azından bunu büyük ölçüde küçültmeniz) tavsiye edilir, ancak buradaki sorunuzun özüyle ilgisi yoktur, bu yüzden bu noktayı atlıyorum.

4 candied_orange Aug 17 2020 at 15:03

Veri kaynaklarının farkında olan bir sınıf, birim testi yapılamaz. Yalnızca entegrasyon testi yapılabilir.

Veri yapılarının farkında olan bir sınıf, birim testine tabi tutulabilir. İhtiyacınız olan şey, veri kaynakları hakkında bilgi gerektirmeyen veri yapılarını sağlamanın bir yoludur.

Bunlar, sabit kodlanmış test verilerinden bellek içi veritabanlarına kadar her şey olabilir. Ancak gerçek veri kaynaklarınızla konuşuyorsanız, birim testi yapamazsınız .