หน่วยการทดสอบคลาสที่ร้องขอข้อมูลจากหลายแหล่ง
บริบท
ฉันกำลังทำโปรเจ็กต์ที่ดึงข้อมูลจาก AWS โดยใช้ AWS SDK ต่างๆสำหรับ. NET ตัวอย่างเฉพาะนี้เกี่ยวข้องกับAWSSDK.IdentityManagement
SDK
เป้าหมายคือการค้นหาข้อมูล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);
}
คำตอบ
วิธีนี้ไม่มีอะไรผิดปกติ มีการดึงข้อมูลจำนวนมาก แต่บางครั้งก็เป็นสิ่งที่ต้องทำ (เช่นการรายงานหรือการเตรียมการถ่ายโอนข้อมูลจำนวนมาก)
หลีกเลี่ยงไม่ได้ที่เมื่อคุณล้อเลียนแหล่งข้อมูลยิ่งคุณมีแหล่งข้อมูลมากเท่าไหร่คุณก็ยิ่งต้องล้อเลียนมากขึ้นเท่านั้น ที่หลีกเลี่ยงไม่ได้ง่ายๆ อย่างไรก็ตามคุณสามารถประเมินแนวทางของคุณใหม่ที่นำคุณมาที่นี่ได้
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 ขอแนะนำว่าอย่าใช้อินเทอร์เฟซจากไลบรารีของคุณในโดเมนของคุณ (หรืออย่างน้อยก็ย่อให้เล็กที่สุด) แต่ไม่เกี่ยวข้องกับหลักของคำถามของคุณที่นี่ดังนั้นฉันจึงข้ามประเด็นนั้นไป
คลาสที่รับรู้แหล่งข้อมูลไม่สามารถทดสอบหน่วยได้ สามารถทดสอบการผสานรวมเท่านั้น
คลาสที่ตระหนักถึงโครงสร้างข้อมูลสามารถทดสอบหน่วยได้ สิ่งที่คุณต้องการคือวิธีจัดเตรียมโครงสร้างข้อมูลที่ไม่จำเป็นต้องมีความรู้เกี่ยวกับแหล่งข้อมูล
สิ่งเหล่านี้สามารถเป็นได้ทุกอย่างตั้งแต่ข้อมูลการทดสอบฮาร์ดโค้ดไปจนถึงในฐานข้อมูลหน่วยความจำ แต่ถ้าคุณกำลังพูดคุยกับแหล่งข้อมูลที่แท้จริงของคุณ, คุณ Aint ทดสอบหน่วย