52

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

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

また、ジレンマを示していると思われる例を次に示します。単体テストは、入力が有効であると主張する保護された関数に対して無効な入力を渡します。単体テストは存在すべきではありませんか? 公務ではありません。おそらく、入力をチェックするとパフォーマンスが低下するでしょうか?または、アサートが存在しない必要がありますか? 関数は非公開ではなく保護されているため、安全のために入力をチェックする必要があります。

4

12 に答える 12

41

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

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

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

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.
}
于 2010-06-30T14:16:35.083 に答える
18

私見 debug.asserts 岩。このすばらしい記事では、単体テスト プロジェクトに app.config を追加し、ダイアログ ボックスを無効にすることで、単体テストが中断されないようにする方法を示しています。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.diagnostics>
    <assert assertuienabled="false"/>
</system.diagnostics>
于 2014-08-24T20:48:02.027 に答える
10

他の人が述べたように、デバッグアサートは常に true であるべきものを対象としています。(これの凝った用語はinvariantsです)。

単体テストがアサートをトリップする偽のデータを渡している場合、自問自答する必要があります-なぜそれが起こっているのですか?

  • テスト対象の関数が偽のデータを処理することになっている場合、明らかにそのアサートは存在しないはずです。
  • 関数がその種のデータを処理する機能を備えていない場合(アサートによって示される)、なぜ単体テストを行うのでしょうか?

2 番目の点は、かなりの数の開発者が陥っているように見える点です。コードが処理するように構築されているすべてのものを単体テストし、それ以外のすべてについて例外をアサートまたはスローします。あなたは起こると思いますか?
「未定義の動作」について話している C/C++ ドキュメントの部分を知っていますか? これです。懸命に保釈し、保釈します。


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

于 2010-04-07T20:45:04.957 に答える
8

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

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

于 2009-01-04T00:55:41.017 に答える
2

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

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

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

単体テストにアサート テストを含めることもできます。

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

于 2009-01-03T22:08:19.887 に答える
1

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

非パブリック関数をテストしているので、関数が無効な引数で呼び出されるリスクは何ですか? あなたの単体テストはそのリスクをカバーしていませんか? TDD (テスト駆動開発) 手法に従ってコードを記述する場合は、そうすべきです。

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

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

于 2009-01-04T17:39:01.647 に答える
1

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

興味深い質問です。これらのアサートは、本番環境にデプロイするときに実行時にオフになることが多いと理解しているからです。(ちょっと目的を破りますが、それは別の問題です。)

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

于 2009-01-03T22:09:29.260 に答える
1

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

ここでの問題は、エラーと問題を区別していないことです。

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

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

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

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

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

于 2013-08-25T14:44:46.137 に答える
0

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

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

これをテストすることはできないはずです(実際にできることは何もないので、おそらくテストしたくないでしょう!)

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

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

コード内のこの種の「アサーション」は、まだ非常にテスト可能です。

PK :-)

于 2010-04-07T21:08:29.033 に答える