불변 개체에서 인수 유효성 검사 및 필드 초기화를 테스트하는 더 쉬운 방법이 있습니까?


20

내 도메인은 다음과 같은 간단한 불변 클래스로 구성됩니다.

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        if (fullName == null)
            throw new ArgumentNullException(nameof(fullName));
        if (nameAtBirth == null)
            throw new ArgumentNullException(nameof(nameAtBirth));
        if (taxId == null)
            throw new ArgumentNullException(nameof(taxId));
        if (phoneNumber == null)
            throw new ArgumentNullException(nameof(phoneNumber));
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        FullName = fullName;
        NameAtBirth = nameAtBirth;
        TaxId = taxId;
        PhoneNumber = phoneNumber;
        Address = address;
    }
}

null 검사와 속성 초기화를 작성하는 것은 이미 매우 지루하지만 현재 는 각 클래스에 대한 단위 테스트 를 작성 하여 인수 유효성 검사가 올바르게 작동하고 모든 속성이 초기화되었는지 확인합니다. 이것은 유익하지 않은 혜택으로 극도로 지루한 바쁜 일과 같습니다.

실제 솔루션은 C #이 기본적으로 불변성과 null이 아닌 참조 유형을 지원하는 것입니다. 그러나 그 동안 상황을 개선하기 위해 어떻게해야합니까? 이 모든 테스트를 쓸 가치가 있습니까? 각 클래스에 대한 테스트 작성을 피하기 위해 해당 클래스에 대한 코드 생성기를 작성하는 것이 좋은 생각입니까?


여기에 내가 대답을 바탕으로 한 것이 있습니다.

null 확인 및 속성 초기화를 단순화하여 다음과 같이 할 수 있습니다.

FullName = fullName.ThrowIfNull(nameof(fullName));
NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
TaxId = taxId.ThrowIfNull(nameof(taxId));
PhoneNumber = phoneNumber.ThrowIfNull(nameof(phoneNumber));
Address = address.ThrowIfNull(nameof(address));

Robert Harvey 의 다음 구현 사용 :

public static class ArgumentValidationExtensions
{
    public static T ThrowIfNull<T>(this T o, string paramName) where T : class
    {
        if (o == null)
            throw new ArgumentNullException(paramName);

        return o;
    }
}

GuardClauseAssertionfrom을 사용하여 null 검사를 테스트하는 것은 쉽습니다 AutoFixture.Idioms( Esben Skov Pedersen 제안에 감사드립니다 ).

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(typeof(Address).GetConstructors());

이것은 더 압축 될 수 있습니다 :

typeof(Address).ShouldNotAcceptNullConstructorArguments();

이 확장 방법을 사용하여 :

public static void ShouldNotAcceptNullConstructorArguments(this Type type)
{
    var fixture = new Fixture().Customize(new AutoMoqCustomization());
    var assertion = new GuardClauseAssertion(fixture);

    assertion.Verify(type.GetConstructors());
}

3
제목 / 태그는 객체의 구성 후 단위 테스트를 의미하는 필드 값의 (단위) 테스트에 대해 설명하지만 코드 스 니펫은 입력 인수 / 매개 변수 유효성 검사를 보여줍니다. 이것들은 같은 개념이 아닙니다.
Erik Eidt

2
참고로, 이러한 종류의 상용구를 쉽게 만들 수있는 T4 템플릿을 작성할 수 있습니다. (당신은 또한 == null이 넘어 string.IsEmpty ()을 고려할 수 있습니다.)
에릭 Eidt에게

1
자동 고정 관용구를 살펴 보셨습니까? nuget.org/packages/AutoFixture.Idioms
Esben Skov Pedersen


1
기본 허용 모드가없는 것처럼 보이지만 Fody / NullGuard를 사용할 수도 있습니다 .

답변:


5

이런 종류의 경우를 위해 정확히 t4 템플릿을 만들었습니다. 불변 클래스에 대한 상용구를 많이 작성하지 않기 위해.

https://github.com/xaviergonz/T4Immutable T4Immutable은 불변 클래스에 대한 코드를 생성하는 C # .NET 앱용 T4 템플릿입니다.

이것을 사용하면 null이 아닌 테스트에 대해 구체적으로 이야기합니다.

[PreNotNullCheck, PostNotNullCheck]
public string FirstName { get; }

생성자는 다음과 같습니다.

public Person(string firstName) {
  // pre not null check
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  // assignations + PostConstructor() if needed

  // post not null check
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

이것을 말했지만 널 검사에 JetBrains Annotations를 사용하면 다음과 같이 할 수도 있습니다.

[JetBrains.Annotations.NotNull, ConstructorParamNotNull]
public string FirstName { get; }

그리고 생성자는 다음과 같습니다.

public Person([JetBrains.Annotations.NotNull] string firstName) {
  // pre not null check is implied by ConstructorParamNotNull
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  FirstName = firstName;
  // + PostConstructor() if needed

  // post not null check implied by JetBrains.Annotations.NotNull on the property
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

또한 이것보다 몇 가지 더 많은 기능이 있습니다.


고맙습니다. 방금 시도했지만 완벽하게 작동했습니다. 원래 질문의 모든 문제를 해결하므로이 답변을 합격 답변으로 표시합니다.
Botond Baláss

16

모든 울타리를 작성하는 문제를 완화 할 수있는 간단한 리팩토링으로 약간의 개선을 얻을 수 있습니다. 먼저 다음 확장 방법이 필요합니다.

internal static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
    if (o == null)
        throw new ArgumentNullException(paramName);

    return o;
}

그런 다음 쓸 수 있습니다 :

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        FullName = fullName.ThrowIfNull(nameof(fullName));
        NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
        TaxId = taxId.ThrowIfNull(nameof(taxId));
        PhoneNumber = phoneNumber.ThrowIfNull(nameof(fullName));
        Address = address.ThrowIfNull(nameof(address));
    }
}

확장 메서드에서 원래 매개 변수를 반환하면 유연한 인터페이스가 만들어 지므로 원하는 경우 다른 확장 메서드로이 개념을 확장하고 할당에서 모두 연결할 수 있습니다.

다른 기술은 개념적으로 더 우아하지만 매개 변수를 [NotNull]속성 으로 장식하고 이와 같이 Reflection을 사용 하는 것과 같이 점차 실행이 더 복잡 합니다 .

즉, 클래스가 공개 API의 일부가 아닌 한 이러한 테스트가 모두 필요한 것은 아닙니다.


3
이것이 다운 보트 된 것입니다. Roslyn에 의존하지 않는다는 점을 제외하고 가장 많이 대답 한 답변이 제공하는 접근 방식과 매우 유사합니다.
Robert Harvey

내가 싫어하는 점은 생성자 수정에 취약하다는 것입니다.
jpmc26

@ jpmc26 : 그게 무슨 뜻입니까?
Robert Harvey

1
귀하의 답변을 읽으 면서 생성자를 테스트하지 말고 대신 각 매개 변수에 대해 호출되는 일반적인 방법을 만들고 테스트하십시오. 새 속성 / 인수 쌍을 추가하고 null생성자에 대한 인수를 테스트하는 것을 잊어 버린 경우 테스트 실패가 발생하지 않습니다.
jpmc26

@ jpmc26 : 당연히. 그러나 OP에 설명 된 것처럼 기존의 방식으로 작성하는 경우에도 마찬가지입니다. 일반적인 방법은 throw생성자 외부로 옮기는 것 입니다. 실제로 생성자 내에서 다른 곳으로 제어를 전송하지 않습니다. 그것은 단지 유틸리티 방법이며 , 원하는 경우 유창하게 작업하는 방법 입니다.
Robert Harvey

7

단기적으로는 그러한 테스트를 작성하는 지루함에 대해 할 수있는 일이 많지 않습니다. 그러나 다음 C # (v7) 릴리스의 일부로 구현 될 예정이므로 다음 몇 달 내에 발생할 수있는 throw 표현식에 도움이 될 수 있습니다.

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        FullName = fullName ?? throw new ArgumentNullException(nameof(fullName));
        NameAtBirth = nameAtBirth ?? throw new ArgumentNullException(nameof(nameAtBirth));
        TaxId = taxId ?? throw new ArgumentNullException(nameof(taxId)); ;
        PhoneNumber = phoneNumber ?? throw new ArgumentNullException(nameof(phoneNumber)); ;
        Address = address ?? throw new ArgumentNullException(nameof(address)); ;
    }
}

Try Roslyn webapp을 통해 throw 표현식을 실험 할 수 있습니다 .


훨씬 더 컴팩트합니다. 감사합니다. 반 줄. C # 7을 기대합니다!
Botond Baláss

@ BotondBalázs 당신이 원한다면 지금 VS15 미리보기에서 시도해 볼 수 있습니다
user1306322

7

아무도 NullGuard를 언급 하지 않은 것에 놀랐습니다 . NuGet을 통해 사용할 수 있으며 컴파일 시간 동안 null 검사를 IL에 자동으로 습니다.

따라서 생성자 코드는 단순히

public Person(
    string fullName,
    string nameAtBirth,
    string taxId,
    PhoneNumber phoneNumber,
    Address address)
{
    FullName = fullName;
    NameAtBirth = nameAtBirth;
    TaxId = taxId;
    PhoneNumber = phoneNumber;
    Address = address;
}

NullGuard는 사용자가 작성한 것과 정확히 일치하도록 null 검사를 추가합니다.

그러나 NullGuard는 옵트 아웃입니다 . 즉 , 속성과 함께 null 값 을 명시 적으로 허용 하지 않는 한 모든 null 메서드 를 모든 메서드 및 생성자 인수, 속성 getter 및 setter에 추가하고 메서드 반환 값을 확인 합니다.[AllowNull]


감사합니다. 이것은 매우 훌륭한 일반적인 솔루션처럼 보입니다. 불행히도 기본적으로 언어 동작을 수정하는 것은 매우 유감입니다. [NotNull]null을 기본값으로 허용하지 않고 속성 을 추가하여 작동하면 훨씬 더 싶습니다 .
Botond Baláss

@ BotondBalázs : PostSharp 는 다른 파라미터 검증 속성과 함께 이것을 가지고 있습니다. Fody와 달리 무료는 아닙니다. 또한 생성 된 코드는 PostSharp DLL을 호출하므로 제품에 포함해야합니다. Fody는 컴파일 중에 만 코드를 실행하며 런타임에는 필요하지 않습니다.
로마 라이너

@ BotondBalázs : 처음에는 NullGuard가 기본값마다 적용되는 것이 이상하다는 것을 알았지 만 실제로 의미가 있습니다. 합리적인 코드 기반에서 null을 허용하는 것은 null을 확인하는 것보다 훨씬 덜 일반적입니다. 당신이 경우 정말 널 어딘가를 허용 할 옵트 아웃 방식의 힘 당신은 매우 철저합니다.
로마 라이너 2017

5
그렇습니다. 그러나 합리적이지만 최소한의 놀라움원칙에 위배 됩니다. 새로운 프로그래머가 프로젝트가 NullGuard를 사용한다는 것을 모른다면 큰 놀라움을 안겨줍니다! 그건 그렇고, downvote도 이해하지 못합니다. 이것은 매우 유용한 대답입니다.
Botond Baláss

1
NullGuard 같은 @ BotondBalázs / 로마, 외모는 기본적으로 작동하는 방식을 변경하는 새로운 명시 적 모드가
카일 W

5

이 모든 테스트를 쓸 가치가 있습니까?

아마 아닐 것입니다. 당신이 그것을 망칠 가능성은 무엇입니까? 어떤 의미론이 당신 아래에서 변할 가능성은 무엇입니까? 누군가 그것을 망치면 어떤 영향이 있습니까?

거의 깨지지 않을 무언가에 대한 테스트를하는 데 많은 시간을 소비하고 있다면 그럴 경우 사소한 해결책입니다 ... 아마도 가치가 없습니다.

각 클래스에 대한 테스트 작성을 피하기 위해 해당 클래스에 대한 코드 생성기를 작성하는 것이 좋은 생각입니까?

아마도? 그런 종류의 일은 반성을 통해 쉽게 할 수 있습니다. 고려해야 할 것은 실제 코드에 대한 코드 생성을 수행하는 것이므로 사람 오류가있을 수있는 N 클래스가 없습니다. 버그 예방> 버그 탐지.


고맙습니다. 어쩌면 내가 내 질문에 충분히 명확하지 않았다 그러나 나는하지 그들을 위해 테스트, 불변 클래스를 생성하는 발전기를 작성하는 것을 의미
Botond는 발라

2
나는 또한 if...throw그들이 가질 수 있는 대신 여러 번 보았습니다 ThrowHelper.ArgumentNull(myArg, "myArg"). 논리가 여전히 존재하고 코드 범위에 얽매이지 않습니다. 를 Where ArgumentNull방법은 할 것입니다 if...throw.
Matthew

2

이 모든 테스트를 쓸 가치가 있습니까?

아니요
. 클래스가 사용되는 다른 로직 테스트를 통해 해당 속성을 테스트했다고 확신합니다.

예를 들어, 해당 속성을 기반으로 어설 션이있는 팩토리 클래스에 대한 테스트를 가질 수 있습니다 ( Name예를 들어 올바르게 지정된 속성으로 작성된 인스턴스 ).

이러한 클래스가 일부 제 3 자 / 최종 사용자가 사용하는 공개 API에 노출 된 경우 (@EJoshua가 알아서 주셔서 감사합니다) 예상되는 테스트 ArgumentNullException가 유용 할 수 있습니다.

C # 7을 기다리는 동안 확장 방법을 사용할 수 있습니다

public MyClass(string name)
{
    name.ThrowArgumentNullExceptionIfNull(nameof(name));
}

public static void ThrowArgumentNullExceptionIfNull(this object value, string paramName)
{
    if(value == null)
        throw new ArgumentNullException(paramName);
}

테스트를 위해 리플렉션을 사용 null하여 모든 매개 변수에 대한 참조 를 작성 하고 예상되는 예외에 대해 주장 하는 하나의 매개 변수화 된 테스트 방법을 사용할 수 있습니다 .


1
그것은 속성 초기화를 테스트하지 않는다는 확실한 주장입니다.
Botond Baláss

코드 사용 방법에 따라 다릅니다. 코드가 공개 라이브러리의 일부인 경우 다른 사람들이 라이브러리에 대한 방법을 알도록 맹목적으로 신뢰해서는 안됩니다 (특히 소스 코드에 액세스 할 수없는 경우). 그러나 자체 코드를 작동시키기 위해 내부적으로 사용하는 것이면 자체 코드를 사용하는 방법을 알아야하므로 검사는 목적이 적습니다.
EJoshuaS-복원 Monica Monica

2

항상 다음과 같은 메소드를 작성할 수 있습니다.

// Just to illustrate how to call this
private static void SomeMethod(string a, string b, string c, string d)
    {
        ValidateArguments(a, b, c, d);
        // ...
    }

    // This is the one to use as a utility function
    private static void ValidateArguments(params object[] args)
    {
        for (int i = 0; i < args.Length; i++)
        {
            if (args[i] == null)
            {
                StackTrace trace = new StackTrace();
                // Get the method that called us
                MethodBase info = trace.GetFrame(1).GetMethod();

                // Get information on the parameter that is null so we can add its name to the exception
                ParameterInfo param = info.GetParameters()[i];

                // Raise the exception on behalf of the caller
                throw new ArgumentNullException(param.Name);
            }
        }
    }

이런 종류의 유효성 검사가 필요한 여러 가지 방법이 있다면 최소한 입력을 절약 할 수 있습니다. 물론이 솔루션은 메소드의 매개 변수 중 어느 것도 될 수 없다고null 하지만 원하는 경우이를 변경하도록 수정할 수 있습니다.

다른 유형별 유효성 검사를 수행하도록이를 확장 할 수도 있습니다. 예를 들어 문자열이 공백이거나 공백 일 수 없다는 규칙이있는 경우 다음 조건을 추가 할 수 있습니다.

// Note that we already know based on the previous condition that args[i] is not null
else if (args[i].GetType() == typeof(string))
            {
                string argCast = arg as string;

                if (!argCast.Trim().Any())
                {
                    ParameterInfo param = GetParameterInfo(i);

                    throw new ArgumentException(param.Name + " is empty or consists only of whitespace");
                }
            }

내가 참조하는 GetParameterInfo 메소드는 기본적으로 리플렉션을 수행하므로 DRY 원칙을 위반하는 동일한 것을 계속 입력 할 필요가 없습니다 .

private static ParameterInfo GetParameterInfo(int index)
    {
        StackTrace trace = new StackTrace();

        // Note that we have to go 2 methods back to get the ValidateArguments method's caller
        MethodBase info = trace.GetFrame(2).GetMethod();

        // Get information on the parameter that is null so we can add its name to the exception
        ParameterInfo param = info.GetParameters()[index];

        return param;
    }

1
이것이 다운 보트 인 이유는 무엇입니까? 그리 나쁘지
않아요

0

생성자에서 예외를 던지는 것은 권장하지 않습니다. 문제는 테스트와 관련이없는 유효한 매개 변수를 전달해야하므로 테스트하기가 어렵다는 것입니다.

예를 들어, 세 번째 매개 변수에서 예외가 발생하는지 테스트하려면 첫 번째와 두 번째 매개 변수를 올바르게 전달해야합니다. 따라서 테스트는 더 이상 격리되지 않습니다. 첫 번째와 두 번째 매개 변수의 제약 조건이 변경되면이 테스트 사례는 세 번째 매개 변수를 테스트하더라도 실패합니다.

Java 유효성 검사 API를 사용하고 유효성 검사 프로세스를 외부화하는 것이 좋습니다. 다음과 같은 네 가지 책임 (클래스)을 제안합니다.

  1. Java 유효성 검증 주석이있는 제안 오브젝트는 ConstraintViolations 세트를 리턴하는 validate 메소드로 매개 변수에 주석이 달린 매개 변수를 사용했습니다. 한 가지 장점은 유효한 주장없이이 개체를 전달할 수 있다는 것입니다. 도메인 개체를 인스턴스화하지 않고 필요할 때까지 유효성 검사를 지연시킬 수 있습니다. 유효성 검사 개체는 레이어 별 지식이없는 POJO이므로 다른 레이어에서 사용할 수 있습니다. 공개 API의 일부가 될 수 있습니다.

  2. 유효한 객체를 만드는 도메인 객체의 팩토리. 제안 오브젝트를 전달하면 팩토리가 유효성 검증을 호출하고 모든 것이 정상인 경우 도메인 오브젝트를 작성합니다.

  3. 다른 개발자의 인스턴스화를 위해 대부분 액세스 할 수없는 도메인 객체 자체입니다. 테스트 목적으로이 클래스를 패키지 범위에 두는 것이 좋습니다.

  4. 구체적 도메인 개체를 숨기고 잘못 사용하기위한 퍼블릭 도메인 인터페이스.

null 값을 확인하지 않는 것이 좋습니다. 도메인 계층에 들어가 자마자 단일 객체에 대한 종료 또는 검색 기능이있는 객체 체인이없는 한 null을 전달하거나 null을 반환하지 않습니다.

또 다른 요점 : 가능한 한 적은 코드를 작성하는 것은 코드 품질에 대한 척도가 아닙니다. 32k 게임 경쟁에 대해 생각해보십시오. 이 코드는 기술적 인 문제에만 관심이 있고 의미론에는 신경 쓰지 않기 때문에 가장 간결하지만 가장 지저분한 코드입니다. 그러나 의미론은 사물을 포괄적으로 만드는 요점입니다.


1
나는 당신의 마지막 요점에 정중하게 동의하지 않습니다. 100 시간 동안 같은 상용구 입력 할 필요하지 않습니다 도움이 실수의 가능성을 줄일 수 있습니다. 이것이 C #이 아닌 Java 인 경우를 상상해보십시오. 각 속성에 대해 지원 필드와 getter 메서드를 정의해야합니다. 코드는 완전히 읽을 수 없게되며 중요한 인프라는 모호한 인프라에 의해 가려 질 수 있습니다.
Botond Baláss
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.