여러 소스에서 데이터를 요청하는 클래스 단위 테스트

Aug 17 2020

문맥

저는 다양한 .NET 용 AWS SDK를 사용하여 AWS에서 데이터를 가져 오는 프로젝트를 진행하고 있습니다. 이 특정 예제는 AWSSDK.IdentityManagementSDK 를 다룹니다.

목표는 정보를 쿼리 IAmazonIdentityManagementService하고 내가 작업중인 비즈니스 도메인에 도움이되는 모델에 매핑하는 것입니다.

나는 IamService수업에 대한 단위 테스트를 작성하는 임무를 맡았습니다 .

문제

단위 테스트 설정이 너무 장황하기 때문에 I 'm Unit Testing ( GetIamSummaryAsync) 방법이 잘못 구성되어야 한다고 생각할 수밖에 없습니다.

"여러 데이터 소스를 단일 개체에 매핑하기위한 디자인 패턴"과 같은 것을 검색해 보았지만 내가 본 유일한 조언은 어댑터 또는 프록시 패턴을 사용하는 것입니다. 이 시나리오에 적용하는 방법을 잘 모르겠습니다.

질문

  • IamService쉽게 (더 간결하게) 테스트 할 수 있도록 클래스를 구성 할 수있는 더 좋은 방법이 있습니까?
  • 어댑터 또는 프록시 패턴이 이러한 유형의 시나리오에 적합한 경우 어떻게 적용됩니까?
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; }
}

내가 직면 한 문제는 내 단위 테스트가 조립 섹션에서 대량의 코드로 바뀐다는 것입니다. 각 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.이 데이터를 결합해야합니까?

스스로에게 물어볼 첫 번째 질문은이 데이터를 결합해야하는지 여부입니다. 그렇지 않고이 데이터를 별도로 유지할 수 있다면 코드베이스를 더 간단하고 쉽게 모의 (따라서 테스트) 할 수있는 좋은 방법입니다.
이 데이터를 어느 시점에서 결합해야하는 경우 클래스를 리팩토링하면 데이터 결합 논리가 다른 수준으로 이동합니다. 여기서 동일한 단위 테스트 질문이 나타납니다. 해당 레이어 에서 어떻게 조롱 할까요? 논리를 재배치해도 문제가 해결되지 않습니다.

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

데이터 소스를 인식하는 클래스는 단위 테스트 할 수 없습니다. 통합 테스트 만 가능합니다.

데이터 구조를 인식하는 클래스는 단위 테스트가 가능합니다. 필요한 것은 데이터 소스에 대한 지식이 필요하지 않은 데이터 구조를 제공하는 방법입니다.

이들은 하드 코딩 된 테스트 데이터에서 메모리 데이터베이스에 이르기까지 모든 것이 될 수 있습니다. 그러나 실제 데이터 소스와 이야기하는 경우 단위 테스트가 아닙니다 .