web-dev-qa-db-ja.com

不変オブジェクトで引数の検証とフィールドの初期化をテストする簡単な方法はありますか?

私のドメインは、次のような多くの単純な不変クラスで構成されています。

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;
    }
}

Nullチェックのテストは、AutoFixture.IdiomsGuardClauseAssertionを使用すると簡単です(提案ありがとうございます 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());
}
21
Botond Balázs

このような場合にぴったりの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アノテーションを使用する場合は、次のようにすることもできます。

[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));
}

また、これよりもいくつかの機能があります。

5
Javier G.

これらのフェンスをすべて作成する問題を緩和できる単純なリファクタリングを使用して、少し改善することができます。まず、この拡張メソッドが必要です。

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]属性でパラメーターを装飾したり、 this のようなリフレクションを使用したりするなど、実行が徐々に複雑になります。

つまり、クラスが公開APIの一部でない限り、これらすべてのテストが必要なわけではありません。

16
Robert Harvey

短期的には、そのようなテストを書く面倒さについてできることはあまりありません。ただし、C#(v7)の次のリリースの一部として実装される予定のスロー式にはいくつかの助けがあります。

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 でスロー式を試すことができます。

7
David Arno

NullGuard.Fody に誰も言及していないことに驚いています。これは 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はopt-outです。つまり、everyメソッドとコンストラクタの引数、プロパティのゲッターとセッター、さらにメソッドの戻り値をチェックする場合、明示的にallowを使用してnull値を[AllowNull]属性。

7
Roman Reiner

これらすべてのテストを書く価値はありますか?

いいえ、おそらく違います。あなたがそれを台無しにする可能性は何ですか?一部のセマンティクスがあなたの下から変化する可能性はどれくらいですか?誰かがそれを台無しにした場合、どのような影響がありますか?

めったに壊れないものをテストするために多くの時間を費やしている場合、壊れたとしてもささいな修正です...多分それだけの価値はありません。

このようなクラスのコードジェネレーターを記述して、クラスごとにテストを記述しないようにするのは良い考えでしょうか。

多分?そのようなことは、反射で簡単に行うことができます。考慮すべきことは、実際のコードのコード生成を行うことです。そのため、人的エラーが発生する可能性のあるNクラスはありません。バグ防止>バグ検出。

5
Telastyn

あなたはいつも次のようなメソッドを書くことができます:

// 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);
            }
        }
    }

この種類の検証を必要とするいくつかのメソッドがある場合、少なくとも、これにより、いくつかの入力を節約できます。もちろん、このソリューションでは、メソッドのパラメーターのnonenullにできると想定していますが、これを変更して、変更することもできます。欲望。

これを拡張して、他のタイプ固有の検証を実行することもできます。たとえば、文字列を完全に空白または空にすることはできないというルールがある場合、次の条件を追加できます。

// 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;
    }

これらすべてのテストを記述する価値はありますか?

番号。
これらのクラスが使用されているロジックの他のいくつかのテストを通じて、これらのプロパティをテストしたことがかなりあると思います。

たとえば、これらのプロパティに基づいてアサーションを持つファクトリクラスのテストを行うことができます(たとえば、適切に割り当てられたNameプロパティで作成されたインスタンス)。

これらのクラスが、サードパーティ/エンドユーザー(@EJoshuaの通知に感謝)が使用するパブリックAPIに公開されている場合は、予想される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​​つのパラメーター化されたテストメソッドを使用できます。

2
Fabio

コンストラクタから例外をスローすることはお勧めしません。問題は、テストに関係ない有効なパラメーターを渡す必要があるため、これをテストするのが難しいことです。

例:3番目のパラメーターが例外をスローするかどうかをテストする場合、1番目と2番目のパラメーターを正しく渡す必要があります。だからあなたのテストはもはや孤立していません。最初と2番目のパラメーターの制約が変更されると、このテストケースは3番目のパラメーターをテストしても失敗します。

Java検証APIを使用し、検証プロセスを外部化することをお勧めします。4つの責任(クラス)を関与させることをお勧めします:

  1. Java検証アノテーション付きパラメーターを持つ検証オブジェクトで、ConstraintViolationsのセットを返す検証メソッドを使用します。1つの利点は、アサーションなしでこのオブジェクトを渡すことができることです。有効になるまで検証を遅らせることができます。ドメインオブジェクトのインスタンス化を試行せずに必要です。検証オブジェクトは、レイヤー固有の知識のないPOJOであるため、さまざまなレイヤーで使用できます。パブリックAPIの一部にすることができます。

  2. 有効なオブジェクトの作成を担当するドメインオブジェクトのファクトリ。提案オブジェクトを渡すと、すべてが問題なければ、ファクトリが検証を呼び出してドメインオブジェクトを作成します。

  3. 他の開発者がインスタンス化するためにほとんどアクセスできないドメインオブジェクト自体。テスト目的で、このクラスをパッケージスコープに含めることをお勧めします。

  4. 具体的なドメインオブジェクトを非表示にし、誤用を困難にするパブリックドメインインターフェイス。

Null値をチェックすることはお勧めしません。ドメインレイヤーに入るとすぐに、単一のオブジェクトの終了または検索機能を持つオブジェクトチェーンがない限り、nullを渡すこともnullを返すこともできなくなります。

もう1つのポイント:可能な限り少ないコードを記述することは、コード品質の測定基準ではありません。 32kゲームの競技について考えてみてください。このコードは最もコンパクトですが、技術的な問題のみを扱い、セマンティクスは考慮しないため、最も厄介なコードになります。しかし、意味論は物事を包括的にするポイントです。

0
oopexpert