这是一个完全有效的问题。
首先,很多人建议您错误地使用断言。我认为许多调试专家会不同意。尽管用断言检查不变量是一种很好的做法,但断言不应仅限于状态不变量。事实上,除了检查不变量之外,许多专家调试器会告诉您断言任何可能导致异常的条件。
例如,考虑以下代码:
if (param1 == null)
throw new ArgumentNullException("param1");
没关系。但是当抛出异常时,堆栈会展开,直到有东西处理异常(可能是一些顶级默认处理程序)。如果此时执行暂停(您可能在 Windows 应用程序中有一个模态异常对话框),您有机会附加一个调试器,但您可能丢失了很多可以帮助您解决问题的信息,因为大部分堆栈已展开。
现在考虑以下几点:
if (param1 == null)
{
Debug.Fail("param1 == null");
throw new ArgumentNullException("param1");
}
现在如果出现问题,会弹出模态断言对话框。执行立即暂停。您可以自由地附加您选择的调试器,并准确调查堆栈上的内容以及系统在确切故障点的所有状态。在发布版本中,您仍然会遇到异常。
现在我们如何处理您的单元测试?
考虑一个单元测试来测试上面包含断言的代码。您想检查当 param1 为空时是否引发了异常。您希望该特定断言失败,但任何其他断言失败都表明有问题。您希望允许特定测试的特定断言失败。
您解决此问题的方式将取决于您使用的语言等。但是,如果您使用.NET,我有一些建议(我实际上没有尝试过,但我会在未来更新帖子):
- 检查 Trace.Listeners。查找 DefaultTraceListener 的任何实例并将 AssertUiEnabled 设置为 false。这会阻止模式对话框弹出。您也可以清除 listeners 集合,但您不会得到任何跟踪。
- 编写您自己的 TraceListener 来记录断言。如何记录断言取决于您。记录失败消息可能不够好,因此您可能需要遍历堆栈以找到断言来自的方法并记录它。
- 测试结束后,检查发生的唯一断言失败是否是您所期望的。如果发生任何其他情况,则测试失败。
对于一个包含代码的 TraceListener 示例,我将搜索 SUPERASSERT.NET 的 SuperAssertListener 并检查其代码。(如果您真的很想使用断言进行调试,那么集成 SUPERASSERT.NET 也是值得的)。
大多数单元测试框架都支持测试设置/拆卸方法。您可能希望添加代码来重置跟踪侦听器并断言在这些区域中没有任何意外的断言失败,以最大限度地减少重复并防止错误。
更新:
这是一个示例 TraceListener,可用于对断言进行单元测试。您应该向 Trace.Listeners 集合添加一个实例。您可能还想提供一些简单的方法来让您的测试能够掌握侦听器。
注意:这要归功于 John Robbins 的 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.
}