Unit Testing một lớp yêu cầu dữ liệu từ nhiều nguồn
Bối cảnh
Tôi đang thực hiện một dự án lấy dữ liệu từ AWS bằng cách sử dụng các AWS SDK khác nhau cho .NET. Ví dụ cụ thể này đề cập đến AWSSDK.IdentityManagementSDK
Mục tiêu là truy vấn thông tin từ đó IAmazonIdentityManagementServicevà ánh xạ nó thành một mô hình hữu ích cho lĩnh vực kinh doanh mà tôi đang làm việc
Tôi được giao nhiệm vụ viết các bài kiểm tra đơn vị cho IamServicelớp.
Vấn đề
Với việc thiết lập Unit Test quá dài dòng, tôi không thể không nghĩ rằng phương pháp I'm Unit Testing ( GetIamSummaryAsync) phải được xây dựng kém.
Tôi đã tìm hiểu về những thứ như "Thiết kế các mẫu để ánh xạ nhiều nguồn dữ liệu đến các đối tượng đơn lẻ", nhưng lời khuyên duy nhất mà tôi thấy là sử dụng các mẫu Bộ điều hợp hoặc Proxy. Tôi không chắc làm thế nào để áp dụng chúng cho tình huống này
Câu hỏi
- Có cách nào tốt hơn để tôi có thể xây dựng
IamServicelớp của mình để làm cho việc kiểm tra dễ dàng hơn (ngắn gọn hơn) không? - Nếu các mẫu Bộ điều hợp hoặc Proxy phù hợp với loại tình huống này, chúng sẽ được áp dụng như thế nào?
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;
}
}
Trong đó lớp IamSummaryModelđược định nghĩa là:
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; }
}
Vấn đề tôi đang gặp phải là Bài kiểm tra đơn vị của tôi biến thành một khối lượng mã trong phần Lắp ráp. Tôi phải bắt chước mọi lệnh gọi mà tôi thực hiện đối với từng phương thức ứng dụng AWS SDK.
Bài kiểm tra đơn vị mẫu
[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);
}
Trả lời
Không có gì sai với phương pháp này. Nó mang lại rất nhiều thông tin, nhưng đôi khi đó là điều phải làm (ví dụ: báo cáo hoặc chuẩn bị truyền dữ liệu lớn).
Không thể tránh khỏi khi bạn chế nhạo nguồn dữ liệu của mình, bạn càng có nhiều nguồn thì bạn càng phải chế nhạo nhiều hơn. Điều đó không dễ dàng tránh được. Tuy nhiên, bạn có thể đánh giá lại cách tiếp cận đã dẫn bạn đến đây.
1. Dữ liệu này có cần được kết hợp không?
Câu hỏi đầu tiên để tự hỏi là liệu việc kết hợp dữ liệu này có cần thiết hay không. Nếu không, và bạn có thể giữ dữ liệu này riêng biệt, thì đó là một cách tuyệt vời để giữ cho cơ sở mã của bạn đơn giản hơn và dễ mô phỏng hơn (và do đó kiểm tra).
Nếu dữ liệu này cần được kết hợp tại một thời điểm nào đó, thì việc cấu trúc lại lớp của bạn chỉ cần chuyển logic kết hợp dữ liệu sang một cấp độ khác, nơi câu hỏi kiểm tra đơn vị tương tự bây giờ bật lên: làm thế nào để mô phỏng trong lớp đó ? Định vị lại logic không sửa chữa nó.
2. Tôi có cần phải kiểm tra đơn vị này không?
Thứ hai, bạn nên đặt câu hỏi nếu thử nghiệm đơn vị có được bảo hành ở đây hay không. Mặc dù không phải tất cả mọi người đều đồng ý (cá nhân tôi, tôi đang ở trong hàng rào), có một lập luận hợp lý IamServiceđể không được kiểm tra đơn vị vì nó không phải là một lớp logic miền, mà thay vào đó nó là một trình bao bọc / ánh xạ của một tài nguyên bên ngoài .
Tương tự, tôi cũng sẽ không kiểm tra lớp ngữ cảnh EntityFramework, trừ khi nó chứa logic nghiệp vụ tùy chỉnh (ví dụ: các trường kiểm tra tự động), vì logic nghiệp vụ đó cần được kiểm tra. Phần còn lại của lớp chỉ là việc triển khai EF, không đảm bảo thử nghiệm.
IamServiceHiện tại của bạn không có bất kỳ logic kinh doanh thực sự nào, vì vậy lập luận để không kiểm tra đơn vị nó là khá mạnh mẽ, theo ý kiến của tôi. Lập luận rằng ánh xạ của IamSummaryModelđối tượng được coi là logic nghiệp vụ, tốt, có thể tranh luận. Tôi không phải lúc nào cũng kiểm tra các ánh xạ tầm thường vì mã tầm thường không nên được kiểm tra (lưu ý: mặc dù tôi tin rằng điều này là đúng, nhưng tôi biết rằng rất dễ sử dụng nhầm nhãn "tầm thường" trên mã không thực sự tầm thường. HÃY CẨN THẬN)
3. Làm cách nào để giảm thiểu nỗ lực chế nhạo?
Nếu bạn đạt đến điểm này, bạn đồng ý rằng cả việc kết hợp dữ liệu và kiểm tra đơn vị lớp của bạn đều cần thiết. Điều đó kết luận một cách hợp lý là cần phải mô phỏng tất cả các nguồn dữ liệu này khi kiểm tra lớp đó. Bây giờ nó đã trở thành một sự thật tất yếu.
Nhưng điều đó không có nghĩa là bạn không thể làm cho cuộc sống của mình dễ dàng hơn bằng cách sử dụng lại / đơn giản hóa logic sắp xếp. Hãy để lớp thử nghiệm của bạn kế thừa từ một lớp cơ sở được sử dụng như một vật cố định hoặc triển khai một thuộc tính chứa vật cố định đã nói. Đối với câu trả lời này, tôi sẽ chọn con đường kế thừa, nhưng cả hai đều hoạt động.
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);
}
}
Đây là một thực hiện rất nhanh chóng của một vật cố định như vậy. Thiết bị cố định đó có thể làm hầu hết các công việc chân cho bạn. Nếu bạn có nhiều hơn một bài kiểm tra, mà tôi cho rằng bạn sẽ làm như vậy, điều này sẽ giảm đáng kể sự phức tạp của việc phải thiết lập nó cho mỗi bài kiểm tra riêng lẻ.
Khi thiết lập mô hình, bạn có thể dựa vào các giá trị bạn đã chọn và làm cho có thể truy cập thông qua các thuộc tính, sau đó bạn có thể sử dụng lại cho logic xác nhận của mình. Ví dụ:
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()
);
}
}
Lưu ý cách tôi thiết lập một phản hồi bị chế nhạo cụ thể và sau đó tôi có thể sử dụng phản hồi bị chế nhạo đó để so sánh với phản hồi thực tế nhận được từ đơn vị đang thử nghiệm của tôi.
Nếu bạn cần viết các bài kiểm tra cụ thể cho các chính sách cụ thể, thì bạn có thể thêm các tham số phương thức nếu cần, ví dụ:
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);
}
}
Có thể bạn sẽ có logic xác nhận phức tạp hơn khi sử dụng các giá trị được chế tạo tùy chỉnh, nhưng đây chỉ là một ví dụ cơ bản.
Lưu ý: như tôi đã đề cập trong nhận xét cho câu trả lời của candied_orange, bạn không nên sử dụng giao diện từ các thư viện trong miền của mình (hoặc ít nhất là giảm thiểu nó), nhưng điều đó không liên quan đến cốt lõi câu hỏi của bạn ở đây nên tôi bỏ qua điểm đó.
Một lớp nhận thức được nguồn dữ liệu không thể được kiểm tra đơn vị. Nó chỉ có thể được kiểm tra tích hợp.
Một lớp biết cấu trúc dữ liệu có thể được kiểm tra đơn vị. Những gì bạn cần là một cách để cung cấp các cấu trúc dữ liệu không yêu cầu kiến thức về nguồn dữ liệu.
Đây có thể là mọi thứ, từ dữ liệu kiểm tra được mã hóa cứng đến trong cơ sở dữ liệu bộ nhớ. Nhưng nếu bạn đang nói chuyện với các nguồn dữ liệu thực của mình, bạn nên thử nghiệm đơn vị .