web-dev-qa-db-ja.com

ユニットテスト中にアサートをデバッグするためのベストプラクティス

単体テストを多用すると、デバッグアサーションの使用が妨げられますか?テスト対象のコードでデバッグアサートが発生するということは、単体テストが存在してはならないか、デバッグアサートが存在してはならないことを意味しているようです。 「1つしか存在できない」というのは合理的な原則のようです。これは一般的な方法ですか?または、単体テスト時にデバッグアサーションを無効にして、統合テストに使用できるようにしますか?

編集:「Assert」を更新してassertをデバッグし、テスト中のコードのアサートと、テストの実行後に状態をチェックする単体テストの行を区別しました。

また、これはジレンマを示していると私が信じる例です。単体テストは、入力が有効であると主張する保護された関数に対して無効な入力に合格します。ユニットテストは存在すべきではありませんか?それは公の機能ではありません。おそらく、入力をチェックするとパフォーマンスが低下しますか?または、アサーションは存在しないはずですか?関数はプライベートではなく保護されているため、入力の安全性を確認する必要があります。

44
Steve Steiner

これは完全に有効な質問です。

まず第一に、多くの人々はあなたがアサーションを間違って使用していることを示唆しています。多くのデバッグ専門家は同意しないと思います。アサーションで不変条件をチェックすることは良い習慣ですが、アサーションは状態不変条件に限定されるべきではありません。実際、多くのエキスパートデバッガーは、不変条件のチェックに加えて、例外を引き起こす可能性のある条件をアサートするように指示します。

たとえば、次のコードについて考えてみます。

if (param1 == null)
    throw new ArgumentNullException("param1");

それはいいです。ただし、例外がスローされると、何かが例外を処理するまでスタックが巻き戻されます(おそらくトップレベルのデフォルトハンドラー)。その時点で実行が一時停止した場合(Windowsアプリにモーダル例外ダイアログがある場合)、デバッガーをアタッチする機会がありますが、問題の修正に役立つ可能性のある多くの情報が失われている可能性があります。スタックのほとんどは巻き戻されています。

ここで、次のことを考慮してください。

if (param1 == null)
{
    Debug.Fail("param1 == null");
    throw new ArgumentNullException("param1");
}

これで問題が発生した場合、モーダルアサートダイアログがポップアップします。実行は瞬時に一時停止します。選択したデバッガーを自由に接続して、スタックの内容と正確な障害点でのシステムのすべての状態を正確に調査できます。リリースビルドでも、例外が発生します。

では、ユニットテストをどのように処理しますか?

アサーションを含む上記のコードをテストする単体テストについて考えてみます。 param1がnullの場合に例外がスローされることを確認する必要があります。特定のアサーションが失敗することを期待しますが、他のアサーションの失敗は何かが間違っていることを示します。特定のテストで特定のアサーションの失敗を許可する必要があります。

これを解決する方法は、使用している言語などによって異なります。ただし、.NETを使用している場合は、いくつかの提案があります(実際には試していませんが、将来的に投稿を更新します)。

  1. Trace.Listenersを確認してください。 DefaultTraceListenerのインスタンスを見つけて、AssertUiEnabledをfalseに設定します。これにより、モーダルダイアログがポップアップしなくなります。リスナーコレクションをクリアすることもできますが、トレースはまったく行われません。
  2. アサーションを記録する独自のTraceListenerを作成します。アサーションをどのように記録するかはあなた次第です。失敗メッセージを記録するだけでは不十分な場合があるため、スタックを調べてアサーションの取得元のメソッドを見つけ、それも記録することをお勧めします。
  3. テストが終了したら、発生したアサーションの失敗が予期していたものだけであることを確認します。他に発生した場合は、テストに失敗します。

そのようなスタックウォークを実行するコードを含むTraceListenerの例として、SUPERASSERT.NETのSuperAssertListenerを検索し、そのコードを確認します。 (アサーションを使用したデバッグに真剣に取り組んでいる場合は、SUPERASSERT.NETを統合することも価値があります)。

ほとんどの単体テストフレームワークは、テストのセットアップ/分解方法をサポートしています。重複を最小限に抑え、間違いを防ぐために、トレースリスナーをリセットし、これらの領域で予期しないアサーションエラーが発生していないことを表明するコードを追加することをお勧めします。

更新:

これは、アサーションの単体テストに使用できるTraceListenerの例です。 Trace.Listenersコレクションにインスタンスを追加する必要があります。また、テストでリスナーを把握するための簡単な方法も提供することをお勧めします。

注:これは、JohnRobbinsのSUPERASSERT.NETのおかげです。

/// <summary>
/// TraceListener used for trapping assertion failures during unit tests.
/// </summary>
public class DebugAssertUnitTestTraceListener : DefaultTraceListener
{
    /// <summary>
    /// Defines an assertion by the method it failed in and the messages it
    /// provided.
    /// </summary>
    public class Assertion
    {
        /// <summary>
        /// Gets the message provided by the assertion.
        /// </summary>
        public String Message { get; private set; }

        /// <summary>
        /// Gets the detailed message provided by the assertion.
        /// </summary>
        public String DetailedMessage { get; private set; }

        /// <summary>
        /// Gets the name of the method the assertion failed in.
        /// </summary>
        public String MethodName { get; private set; }

        /// <summary>
        /// Creates a new Assertion definition.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="detailedMessage"></param>
        /// <param name="methodName"></param>
        public Assertion(String message, String detailedMessage, String methodName)
        {
            if (methodName == null)
            {
                throw new ArgumentNullException("methodName");
            }

            Message = message;
            DetailedMessage = detailedMessage;
            MethodName = methodName;
        }

        /// <summary>
        /// Gets a string representation of this instance.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return String.Format("Message: {0}{1}Detail: {2}{1}Method: {3}{1}",
                Message ?? "<No Message>",
                Environment.NewLine,
                DetailedMessage ?? "<No Detail>",
                MethodName);
        }

        /// <summary>
        /// Tests this object and another object for equality.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            var other = obj as Assertion;

            if (other == null)
            {
                return false;
            }

            return
                this.Message == other.Message &&
                this.DetailedMessage == other.DetailedMessage &&
                this.MethodName == other.MethodName;
        }

        /// <summary>
        /// Gets a hash code for this instance.
        /// Calculated as recommended at http://msdn.Microsoft.com/en-us/library/system.object.gethashcode.aspx
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return
                MethodName.GetHashCode() ^
                (DetailedMessage == null ? 0 : DetailedMessage.GetHashCode()) ^
                (Message == null ? 0 : Message.GetHashCode());
        }
    }

    /// <summary>
    /// Records the assertions that failed.
    /// </summary>
    private readonly List<Assertion> assertionFailures;

    /// <summary>
    /// Gets the assertions that failed since the last call to Clear().
    /// </summary>
    public ReadOnlyCollection<Assertion> AssertionFailures { get { return new ReadOnlyCollection<Assertion>(assertionFailures); } }

    /// <summary>
    /// Gets the assertions that are allowed to fail.
    /// </summary>
    public List<Assertion> AllowedFailures { get; private set; }

    /// <summary>
    /// Creates a new instance of this trace listener with the default name
    /// DebugAssertUnitTestTraceListener.
    /// </summary>
    public DebugAssertUnitTestTraceListener() : this("DebugAssertUnitTestListener") { }

    /// <summary>
    /// Creates a new instance of this trace listener with the specified name.
    /// </summary>
    /// <param name="name"></param>
    public DebugAssertUnitTestTraceListener(String name) : base()
    {
        AssertUiEnabled = false;
        Name = name;
        AllowedFailures = new List<Assertion>();
        assertionFailures = new List<Assertion>();
    }

    /// <summary>
    /// Records assertion failures.
    /// </summary>
    /// <param name="message"></param>
    /// <param name="detailMessage"></param>
    public override void Fail(string message, string detailMessage)
    {
        var failure = new Assertion(message, detailMessage, GetAssertionMethodName());

        if (!AllowedFailures.Contains(failure))
        {
            assertionFailures.Add(failure);
        }
    }

    /// <summary>
    /// Records assertion failures.
    /// </summary>
    /// <param name="message"></param>
    public override void Fail(string message)
    {
        Fail(message, null);
    }

    /// <summary>
    /// Gets rid of any assertions that have been recorded.
    /// </summary>
    public void ClearAssertions()
    {
        assertionFailures.Clear();
    }

    /// <summary>
    /// Gets the full name of the method that causes the assertion failure.
    /// 
    /// Credit goes to John Robbins of Wintellect for the code in this method,
    /// which was taken from his excellent SuperAssertTraceListener.
    /// </summary>
    /// <returns></returns>
    private String GetAssertionMethodName()
    {

        StackTrace stk = new StackTrace();
        int i = 0;
        for (; i < stk.FrameCount; i++)
        {
            StackFrame frame = stk.GetFrame(i);
            MethodBase method = frame.GetMethod();
            if (null != method)
            {
                if(method.ReflectedType.ToString().Equals("System.Diagnostics.Debug"))
                {
                    if (method.Name.Equals("Assert") || method.Name.Equals("Fail"))
                    {
                        i++;
                        break;
                    }
                }
            }
        }

        // Now walk the stack but only get the real parts.
        stk = new StackTrace(i, true);

        // Get the fully qualified name of the method that made the assertion.
        StackFrame hitFrame = stk.GetFrame(0);
        StringBuilder sbKey = new StringBuilder();
        sbKey.AppendFormat("{0}.{1}",
                             hitFrame.GetMethod().ReflectedType.FullName,
                             hitFrame.GetMethod().Name);
        return sbKey.ToString();
    }
}

期待するアサーションの各テストの開始時に、AllowedFailuresコレクションにアサーションを追加できます。

すべてのテストの最後に(ユニットテストフレームワークがテスト分解メソッドをサポートしていることを願っています)、次のことを行います。

if (DebugAssertListener.AssertionFailures.Count > 0)
{
    // TODO: Create a message for the failure.
    DebugAssertListener.ClearAssertions();
    DebugAssertListener.AllowedFailures.Clear();
    // TODO: Fail the test using the message created above.
}
36
Alex Humphrey

IMHOdebug.assertsロック。この すばらしい記事 は、ユニットテストプロジェクトにapp.configを追加し、ダイアログボックスを無効にすることで、ユニットテストの中断を防ぐ方法を示しています。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.diagnostics>
    <assert assertuienabled="false"/>
</system.diagnostics>
12
Avi

コード内のアサーションは、「この条件はこの時点で常に真である必要がある」という読者へのステートメントです(そうあるべきです)。ある程度の規律を持って行われると、コードが正しいことを確認するための一部になる可能性があります。ほとんどの人はそれらをデバッグ印刷ステートメントとして使用します。単体テストは、コードが特定のテストケースを正しく実行することを示すコードです。うまくいかない、彼らは両方とも要件を文書化することができ、コードが本当に正しいというあなたの自信を高めることができます。

違いがありますか?プログラムアサーションはそれを正しくするのに役立ち、単体テストはコードが正しいという他の誰かの自信を育むのに役立ちます。

7
Charlie Martin

他の人が言及したように、デバッグアサートは常に真である必要がありますのことを目的としています。 (これの派手な用語は不変量です)。

ユニットテストがアサーションをトリップしている偽のデータに合格している場合は、自分自身に質問する必要があります-なぜそれが起こっているのですか?

  • テスト中の関数が偽のデータを処理するために想定である場合、明らかにそのアサートはそこにあるべきではありません。
  • 関数がnotその種のデータを処理するように装備されている場合(アサートによって示されるように)、なぜそれを単体テストするのですか?

2つ目のポイントは、かなりの数の開発者が陥っているように見えるポイントです。コードが処理するように構築されているすべてのものを単体テストし、他のすべての例外をアサートまたはスローします-結局のところ、コードがそれらの状況に対処するように構築されておらず、それらを発生させる場合は、どうしますかあなたは起こることを期待していますか?
「未定義の振る舞い」について説明しているC/C++ドキュメントの部分をご存知ですか?これです。保釈と保釈は難しい。


明確にするための更新:これの裏返しは、他の内部のものを呼び出す内部のものにのみDebug.Assertを使用する必要があることに気付くことです。コードがサードパーティに公開されている場合(つまり、ライブラリなど)、期待できる入力に制限はありません。したがって、適切に検証して例外などをスローする必要があります。また、そのための単体テストも行う必要があります。

7
Orion Edwards

優れた単体テストのセットアップには、アサートをキャッチする機能があります。アサーションがトリガーされた場合、現在のテストは失敗し、次のテストが実行されます。

私たちのライブラリでは、TTY/ASSERTSなどの低レベルのデバッグ機能に呼び出されるハンドラーがあります。デフォルトのハンドラーはprintf/breakですが、クライアントコードはさまざまな動作のカスタムハンドラーをインストールできます。

UnitTestフレームワークは、メッセージをログに記録し、アサートで例外をスローする独自のハンドラーをインストールします。 UnitTestコードは、これらの例外が発生した場合にそれらをキャッチし、アサートされたステートメントとともに失敗としてログに記録します。

ユニットテストにアサートテストを含めることもできます-例:.

CHECK_ASSERT(someList.getAt(someList.size()+ 1); //アサートが発生した場合はテストに合格

2
Andrew Grant

「契約によるプログラミング」アサーションに対してC++/Javaアサーションを使用するのですか、それともCppUnit/JUnitアサーションを使用するのですか?その最後の質問は、それが前者であると私に信じさせます。

興味深い質問です。本番環境にデプロイすると、実行時にこれらのアサートがオフになることがよくあると私は理解しています。 (キンダは目的を打ち負かしますが、それは別の質問です。)

あなたがそれをテストするとき、それらはあなたのコードに残されるべきだと私は言うでしょう。前提条件が適切に適用されていることを確認するためのテストを作成します。テストは「ブラックボックス」である必要があります。テストするときは、クラスのクライアントとして行動する必要があります。本番環境でそれらをオフにしても、テストが無効になることはありません。

1
duffymo

単体テストを実施している場合でも、デバッグアサーションを維持する必要があります。

ここでの問題は、エラーと問題を区別することではありません。

関数が誤った引数をチェックする場合、デバッグアサーションが発生することはありません。代わりに、エラー値を返す必要があります。間違ったパラメータで関数を呼び出すとエラーになりました。

関数に正しいデータが渡されたが、ランタイムのメモリが不足しているために正しく動作できない場合、この問題のためにコードはデバッグアサートを発行する必要があります。それが成り立たない場合、「すべての賭けはオフ」であるため、終了しなければならないという基本的な仮定の例です。

あなたの場合、誤った値を引数として提供する単体テストを書いてください。エラーの戻り値(または同様のもの)を期待する必要があります。アサートを取得しますか? -代わりに、コードをリファクタリングしてエラーを生成します。

バグのない問題でもアサートをトリガーできることに注意してください。例えばハードウェアが破損する可能性があります。あなたの質問では、統合テストについて言及しました。実際、誤って構成された統合システムに対してアサートすることは、アサート領域です。例えば互換性のないライブラリバージョンがロードされました。

「デバッグ」-アサートの理由は、勤勉/安全であることと高速/小さいことの間のトレードオフであることに注意してください。

1
Faisal Memon

最初に両方の契約による設計アサーションおよびユニットテストを行うために、ユニットテストフレームワークはアサーションをキャッチできる必要があります。 DbCアボートが原因でユニットテストがアボートした場合、それらを実行することはできません。ここでの代替手段は、単体テストの実行(コンパイルの読み取り)中にこれらのアサーションを無効にすることです。

非公開関数をテストしているので、無効な引数で関数が呼び出されるリスクは何ですか?あなたのユニットテストはそのリスクをカバーしていませんか? TDD(テスト駆動開発)手法に従ってコードを作成する場合は、そうする必要があります。

コードでこれらのDbcタイプのアサーションが本当に必要な場合は、これらのアサーションを持つメソッドに無効な引数を渡す単体テストを削除できます。

ただし、Dbcタイプのアサーションは、粗粒度の単体テストがある場合、低レベルの関数(単体テストによって直接呼び出されない)で役立つ場合があります。

1
philant

この質問が出されてからしばらく経ちましたが、C#コードを使用して単体テスト内からDebug.Assert()呼び出しを検証する別の方法があると思います。 #if DEBUG ... #endifブロックに注意してください。これは、デバッグ構成で実行されていないときにテストをスキップするために必要です(この場合、Debug.Assert()は起動されません)。

[TestClass]
[ExcludeFromCodeCoverage]
public class Test
{
    #region Variables              |

    private UnitTestTraceListener _traceListener;
    private TraceListenerCollection _originalTraceListeners;

    #endregion

    #region TestInitialize         |

    [TestInitialize]
    public void TestInitialize() {
        // Save and clear original trace listeners, add custom unit test trace listener.
        _traceListener = new UnitTestTraceListener();
        _originalTraceListeners = Trace.Listeners;
        Trace.Listeners.Clear();
        Trace.Listeners.Add(_traceListener);

        // ... Further test setup
    }

    #endregion
    #region TestCleanup            |

    [TestCleanup]
    public void TestCleanup() {
        Trace.Listeners.Clear();
        Trace.Listeners.AddRange(_originalTraceListeners);
    }

    #endregion

    [TestMethod]
    public void TheTestItself() {
        // Arrange
        // ...

        // Act
        // ...
        Debug.Assert(false, "Assert failed");



    // Assert

#if DEBUG        
    // NOTE This syntax comes with using the FluentAssertions NuGet package.
    _traceListener.GetWriteLines().Should().HaveCount(1).And.Contain("Fail: Assert failed");
#endif

    }
}

UnitTestTraceListenerクラスは次のようになります。

[ExcludeFromCodeCoverage]
public class UnitTestTraceListener : TraceListener
{
    private readonly List<string> _writes = new List<string>();
    private readonly List<string> _writeLines = new List<string>();

    // Override methods
    public override void Write(string message)
    {
        _writes.Add(message);
    }

    public override void WriteLine(string message)
    {
        _writeLines.Add(message);
    }

    // Public methods
    public IEnumerable<string> GetWrites()
    {
        return _writes.AsReadOnly();
    }

    public IEnumerable<string> GetWriteLines()
    {
        return _writeLines.AsReadOnly();
    }

    public void Clear()
    {
        _writes.Clear();
        _writeLines.Clear();
    }
}
0
DotBert

他の人が述べたように、Debug.Assertステートメントは常にtrueである必要があります。引数が正しくない場合でも、アプリが無効な状態になるのを防ぐために、アサーションはtrueである必要があります。

Debug.Assert(_counter == somethingElse, "Erk! Out of wack!");

あなたはこれをテストすることができないはずです(そしてあなたが本当にできることは何もないのでおそらくテストしたくないでしょう!)

私はかなり離れているかもしれませんが、おそらくあなたが話しているかもしれない主張は「引数の例外」としてより適しているという印象を受けます。

if (param1 == null)
  throw new ArgumentNullException("param1", "message to user")

コード内のこの種の「アサーション」は、依然として非常にテスト可能です。

PK :-)

0
Paul Kohler

単体テストを多用すると、デバッグアサーションの使用が妨げられますか?

いいえ。反対です。単体テストでは、作成したホワイトボックステストの実行中に内部状態を再確認することで、デバッグアサーションの価値が大幅に高まります。 DEBUG対応のコードを出荷することはめったにないため(パフォーマンスがまったく重要でない場合を除く)、単体テスト中にDebug.Assertを有効にすることが不可欠です。 DEBUGコードが実行されるのは2回だけです。これは、1)実際に行う統合テストのごく一部を実行しているとき、すべての善意は別として、2)単体テストを実行しているときです。

Debug.Assertテストを使用してコードをインストルメント化して、記述時に不変条件をチェックするのは簡単です。これらのチェックは、単体テストの実行時に健全性チェックとして機能します。

Assertが行う他のことは、コード内で問題が発生した最初のポイントを正確に指すことです。これにより、ユニットテストdoesが問題を見つけたときのデバッグ時間を大幅に短縮できます。

これにより、単体テストの価値が高まります。

テスト対象のコードでデバッグアサートが発生するということは、単体テストが存在してはならないか、デバッグアサートが存在してはならないことを意味しているようです。

適例。この質問は、実際に起こっていることについてです。正しい?したがって、コードにデバッグアサーションが必要であり、単体テスト中にトリガーする必要があります。ユニットテスト中にデバッグアサーションが起動する可能性は、ユニットテスト中にデバッグアサーションを有効にする必要があることを明確に示しています。

アサート起動とは、テストで内部コードが正しく使用されていない(修正する必要がある)か、テスト対象のコードの一部が他の内部コードを誤って呼び出しているか、基本的な仮定が間違っていることを意味します。あなたはあなたの仮定が間違っていると思うのであなたはテストを書きません、あなたは...実際にあなたはそうします。あなたの仮定の少なくともいくつかはおそらく間違っているので、あなたはテストを書きます。この状況では、冗長性は問題ありません。

「1つしか存在できない」というのは合理的な原則のようです。これは一般的な方法ですか?または、単体テスト時にデバッグアサーションを無効にして、統合テストに使用できるようにしますか?

冗長性は、単体テストの実行時間を損なうだけです。 本当に 100%のカバレッジがある場合、ランタイムが問題になる可能性があります。そうでなければ、私は強く反対しません。テストの途中で自動的に仮定をチェックしても問題はありません。それは実際には「テスト」の定義です。

また、これはジレンマを示していると私が信じる例です。単体テストは、入力が有効であると主張する保護された関数に対して無効な入力に合格します。ユニットテストは存在すべきではありませんか?それは公の機能ではありません。おそらく、入力をチェックするとパフォーマンスが低下しますか?または、アサーションは存在しないはずですか?関数はプライベートではなく保護されているため、入力の安全性を確認する必要があります。

通常、単体テストフレームワークの目的は、不変の仮定に違反した場合のコードの動作をテストすることではありません。言い換えると、作成したドキュメントに「パラメータとしてnullを渡すと、結果は未定義です」と記載されている場合、結果が実際に予測できないことを確認する必要はありません。失敗の結果が明確に定義されている場合、それらは未定義ではなく、1)Debug.Assertであってはならず、2)結果が何であるかを正確に定義し、3)その結果をテストする必要があります。内部デバッグアサーションの品質を単体テストする必要がある場合は、1)アサーションフレームワークをテスト可能なアセットにするというAndrew Grantのアプローチを回答として確認する必要があります。また、2)すばらしいテストカバレッジがあります。そして、これは主にプロジェクトの要件に基づいた個人的な決定だと思います。しかし、私はまだデバッグアサートが不可欠で価値があると思います。

言い換えると、Debug.Assert()は単体テストの値を大幅に増加させ、冗長性は機能です。

0
tekHedd