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


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

nullチェックのテストはGuardClauseAssertionfrom を使用して簡単に行えます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
タイトル/タグは、オブジェクトの構築後のユニットテストを意味するフィールド値の(ユニット)テストについて説明していますが、コードスニペットは入力引数/パラメーターの検証を示しています。これらは同じ概念ではありません。
エリックエイド

2
参考までに、この種の定型文を簡単にするT4テンプレートを作成できます。(string.IsEmpty()beyond == nullを検討することもできます。)
エリックエイド

1
オートフィクスチャのイディオムを見ましたか?nuget.org/packages/AutoFixture.Idioms
エスベンスコフペダーセン


1
Fody / NullGuardも使用できますが、デフォルトの許可モードがないように見えます。
ボブ

回答:


5

このような場合にぴったりのt4テンプレートを作成しました。不変クラスの定型文を大量に書くことを避けるため。

https://github.com/xaviergonz/T4Immutable T4Immutableは、不変クラスのコードを生成するC#.NETアプリ用のT4テンプレートです。

あなたがこれを使用する場合、特に非ヌルテストについて話します:

[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を使用してnullチェックを行う場合は、次のこともできます。

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

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


ありがとうございました。試したところ、完璧に機能しました。元の質問のすべての問題に対処するため、これを承認済みの回答としてマークします。
ボトンバラス

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に依存しないことを除いて、最も支持された回答によって提供されるアプローチに非常に似ています。
ロバートハーヴェイ

私がこれについて気に入らないのは、コンストラクターの変更に対して脆弱であるということです。
jpmc26

@ jpmc26:それはどういう意味ですか?
ロバートハーヴェイ

1
あなたの答えを読んで、コンストラクタをテストしないことを提案していますが、代わりに各パラメータで呼び出される一般的なメソッドを作成してテストします。新しいプロパティ/引数のペアを追加し、nullコンストラクターの引数のテストを忘れた場合、失敗したテストは取得されません。
jpmc26

@ jpmc26:当然です。ただし、OPに示されている従来の方法で記述した場合も同様です。一般的な方法はthrow、コンストラクターの外側に移動するだけです。実際には、コンストラクター内のどこか他の場所に制御を移すわけではありません。それはちょうどユーティリティメソッド、および物事を行うための方法だ流暢あなたがそう選択した場合、。
ロバートハーヴェイ

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


もっとコンパクトに、ありがとう。行の半分。C#7を楽しみにしています!
ボトンバラス

@BotondBalázs必要に応じて、VS15プレビューで今すぐ試すことができます
user1306322

7

誰も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は、作成したものに正確に変換するためのヌルチェックを追加します。

ただし、NullGuardはopt-outであることに注意してください。つまり、すべてのメソッドとコンストラクターの引数、プロパティのゲッターとセッターにこれらのnullチェックを追加し、明示的にメソッドの戻り値もチェックします。し、[AllowNull]属性でnull値を許可しします。


ありがとう、これは非常に素晴らしい一般的な解決策のように見えます。デフォルトで言語の動作を変更することは非常に残念ですが。[NotNull]nullをデフォルトにすることを許可せずに、属性を追加することで機能する場合は、さらにそれが欲しいです。
ボトンバラス

@BotondBalázs:PostSharpには、他のパラメーター検証属性とともにこれがあります。Fodyとは異なり、無料ではありません。また、生成されたコードはPostSharp DLLを呼び出すため、それらを製品に含める必要があります。Fodyはコンパイル時にのみコードを実行し、実行時には必要ありません。
ローマンライナー

@BotondBalázs:また、最初はNullGuardがデフォルトごとに適用されるのはちょっとおかしいと感じましたが、本当に理にかなっています。妥当なコードベースでは、nullを許可することは、nullをチェックするよりもはるかに一般的ではありません。本当にどこかでnullを許可する場合は、オプトアウトアプローチにより、nullを徹底的に使用する必要があります。
ローマンライナー

5
はい、それは理にかなっていますが、それは最小驚きの原則に違反しています。新しいプログラマーがプロジェクトでNullGuardを使用していることを知らない場合、彼らは大きな驚きに直面します!ところで、私も下票を理解していません。これは非常に有用な答えです。
ボトンバラス

1
@BotondBalázs/ Roman、NullGuardにはデフォルトの動作方法を変更する新しい明示的モードがあるようです
Kyle W

5

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

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

まれに壊れることのないテストを作成するのに多くの時間を費やしている場合、それが些細な修正であったとしても、それは価値がないかもしれません。

クラスごとにテストを書くことを避けるために、そのようなクラスのコードジェネレータを書くことは良い考えでしょうか?

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


ありがとうございました。たぶん私は私の質問に明確な十分ではなかったが、私は不変クラスを生成ジェネレータを書くことを意味し、彼らのためにテストしていない
Botondバラージュ

2
私はまた、if...throw彼らが持っているのThrowHelper.ArgumentNull(myArg, "myArg")ではなく、何度も見たことがあります、そのようにロジックはまだそこにあり、あなたはコードカバレッジにdingしないでください。どこArgumentNull方法が行いますif...throw
マシュー

2

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

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

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

これらのクラスが、サードパート/エンドユーザー(@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つのテストメソッドを使用できます。


1
これは、プロパティの初期化をテストしないという説得力のある議論です。
ボトンバラス

それは、コードの使用方法によって異なります。コードが公開ライブラリの一部である場合、他の人があなたのライブラリへのアクセス方法を盲目的に信頼するべきではありません(特にソースコードへのアクセス権がない場合)。ただし、独自のコードを機能させるために内部で使用するだけの場合は、独自のコードの使用方法を知っておく必要があるため、チェックにはあまり意味がありません。
EJoshuaS-モニカの復活

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
なぜこれがダウン投票されているのですか?それはあまりにも悪くはない
Gaspa79

0

コンストラクターから例外をスローすることはお勧めしません。問題は、有効なパラメーターを渡さなければならないため、テストに関係ない場合でも、これをテストするのに苦労することです。

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

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

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

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

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

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

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

もう1つのポイント:できるだけ少ないコードを記述することは、コード品質の指標ではありません。32kゲームのコンペティションについて考えてください。このコードは最もコンパクトですが、最も厄介なコードでもあります。これは、技術的な問題のみを考慮し、セマンティクスを考慮しないためです。しかし、セマンティクスは物事を包括的にするポイントです。


1
私はあなたの最後の点に敬意を表しません。同じボイラープレートを100回入力する必要がないので、間違いを犯す可能性を減らすことができます。これがC#ではなくJavaであると想像してください。各プロパティに対してバッキングフィールドとゲッターメソッドを定義する必要があります。コードは完全に読めなくなり、重要なことは不器用なインフラストラクチャによって隠されます。
ボトンドバラス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.