หน่วยการทดสอบคลาสที่ร้องขอข้อมูลจากหลายแหล่ง

Aug 17 2020

บริบท

ฉันกำลังทำโปรเจ็กต์ที่ดึงข้อมูลจาก AWS โดยใช้ AWS SDK ต่างๆสำหรับ. NET ตัวอย่างเฉพาะนี้เกี่ยวข้องกับAWSSDK.IdentityManagementSDK

เป้าหมายคือการค้นหาข้อมูลIAmazonIdentityManagementServiceและแมปกับโมเดลที่เป็นประโยชน์กับโดเมนธุรกิจที่ฉันทำงานอยู่

ฉันได้รับมอบหมายให้เขียนแบบทดสอบหน่วยสำหรับIamServiceชั้นเรียน

ปัญหา

ด้วยการตั้งค่าการทดสอบหน่วยที่ละเอียดมากฉันจึงอดไม่ได้ที่จะคิดว่าวิธีการที่ฉันกำลังทดสอบหน่วย ( GetIamSummaryAsync) ต้องสร้างขึ้นไม่ดี

ฉันได้ลองใช้สิ่งต่างๆเช่น "รูปแบบการออกแบบสำหรับการแมปแหล่งข้อมูลหลายแหล่งกับออบเจ็กต์เดียว" แต่คำแนะนำเดียวที่ฉันเห็นคือให้ใช้รูปแบบอะแดปเตอร์หรือพร็อกซี ฉันไม่แน่ใจว่าจะนำไปใช้กับสถานการณ์นี้อย่างไร

คำถาม

  • มีวิธีที่ดีกว่านี้ไหมที่ฉันจะสร้างIamServiceชั้นเรียนเพื่อให้ง่ายขึ้น (รวบรัดมากขึ้น) ในการทดสอบ
  • หากรูปแบบ Adapter หรือ Proxy เหมาะสมกับสถานการณ์ประเภทนี้จะนำไปใช้อย่างไร
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; }
}

ปัญหาที่ฉันพบคือการทดสอบหน่วยของฉันเปลี่ยนเป็นรหัสจำนวนมากในส่วน Assemble ฉันต้องล้อเลียนทุกครั้งที่ฉันโทรไปยังวิธีการไคลเอ็นต์ AWS SDK แต่ละวิธี

ตัวอย่างการทดสอบหน่วย

[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. ข้อมูลนี้จำเป็นต้องรวมกันหรือไม่?

คำถามแรกที่ต้องถามตัวเองคือจำเป็นต้องรวมข้อมูลนี้หรือไม่ หากไม่เป็นเช่นนั้นและคุณสามารถแยกข้อมูลนี้ออกจากกันนั่นเป็นวิธีที่ยอดเยี่ยมในการทำให้ codebase ของคุณง่ายขึ้นและง่ายต่อการเยาะเย้ย (และทดสอบ)
หากจำเป็นต้องรวมข้อมูลนี้ในบางจุดการปรับโครงสร้างชั้นเรียนของคุณใหม่เพียงแค่เปลี่ยนตรรกะการรวมข้อมูลไปยังอีกระดับหนึ่งซึ่งตอนนี้คำถามทดสอบหน่วยเดียวกันจะปรากฏขึ้น: จะเยาะเย้ยในเลเยอร์นั้นได้อย่างไร การย้ายตรรกะไม่สามารถแก้ไขได้

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

คลาสที่รับรู้แหล่งข้อมูลไม่สามารถทดสอบหน่วยได้ สามารถทดสอบการผสานรวมเท่านั้น

คลาสที่ตระหนักถึงโครงสร้างข้อมูลสามารถทดสอบหน่วยได้ สิ่งที่คุณต้องการคือวิธีจัดเตรียมโครงสร้างข้อมูลที่ไม่จำเป็นต้องมีความรู้เกี่ยวกับแหล่งข้อมูล

สิ่งเหล่านี้สามารถเป็นได้ทุกอย่างตั้งแต่ข้อมูลการทดสอบฮาร์ดโค้ดไปจนถึงในฐานข้อมูลหน่วยความจำ แต่ถ้าคุณกำลังพูดคุยกับแหล่งข้อมูลที่แท้จริงของคุณ, คุณ Aint ทดสอบหน่วย