これは完全に有効な質問です。
まず第一に、多くの人があなたがアサーションを間違って使用していることを示唆しています。多くのデバッグの専門家は同意しないと思います。アサーションを使用して不変条件をチェックすることは良い習慣ですが、アサーションは状態不変条件に限定されるべきではありません。実際、多くのエキスパートデバッガーは、不変条件のチェックに加えて、例外を引き起こす可能性のある条件をアサートするように指示します。
たとえば、次のコードについて考えてみます。
if (param1 == null)
throw new ArgumentNullException("param1");
それはいいです。ただし、例外がスローされると、何かが例外を処理するまでスタックが巻き戻されます(おそらくトップレベルのデフォルトハンドラー)。その時点で実行が一時停止した場合(Windowsアプリでモーダル例外ダイアログが表示される場合があります)、デバッガーをアタッチする機会がありますが、問題の修正に役立つ可能性のある多くの情報が失われている可能性があります。スタックのほとんどは巻き戻されています。
ここで、次のことを考慮してください。
if (param1 == null)
{
Debug.Fail("param1 == null");
throw new ArgumentNullException("param1");
}
これで、問題が発生した場合、モーダルアサートダイアログがポップアップします。実行は瞬時に一時停止されます。選択したデバッガーを自由に接続して、スタックの内容と正確な障害点でのシステムのすべての状態を正確に調査できます。リリースビルドでも、例外が発生します。
では、ユニットテストをどのように処理しますか?
アサーションを含む上記のコードをテストする単体テストについて考えてみます。param1がnullの場合に例外がスローされることを確認する必要があります。特定のアサーションが失敗することを期待しますが、他のアサーションの失敗は何かが間違っていることを示します。特定のテストに対して特定のアサーションの失敗を許可する必要があります。
これを解決する方法は、使用している言語などによって異なります。ただし、.NETを使用している場合は、いくつかの提案があります(実際には試していませんが、将来的に投稿を更新します)。
- Trace.Listenersを確認してください。DefaultTraceListenerのインスタンスを検索し、AssertUiEnabledをfalseに設定します。これにより、モーダルダイアログがポップアップしなくなります。リスナーコレクションをクリアすることもできますが、トレースはまったく行われません。
- アサーションを記録する独自のTraceListenerを作成します。アサーションをどのように記録するかはあなた次第です。失敗メッセージを記録するだけでは不十分な場合があるため、スタックを調べてアサーションの元となったメソッドを見つけ、それも記録することをお勧めします。
- テストが終了したら、発生したアサーションの失敗が予期していたものだけであることを確認します。他に発生した場合は、テストに失敗します。
そのようなスタックウォークを実行するコードを含む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.
}