是否大量使用单元测试会阻碍调试断言的使用?看起来在测试代码中的调试断言触发意味着单元测试不应该存在或者调试断言不应该存在."只有一个"似乎是一个合理的原则.这是常见做法吗?或者在单元测试时禁用调试断言,以便它们可以进行集成测试?
编辑:我更新了'Assert'以调试assert,以区分测试代码中的断言与测试运行后检查状态的单元测试中的行.
此外,这里有一个我相信显示困境的例子:单元测试为受保护函数传递无效输入,该函数断言它的输入是有效的.单元测试不存在吗?这不是一个公共职能.也许检查输入会杀死perf?或者断言不存在?该功能不受保护,因此应检查其输入是否安全.
这是一个非常有效的问题.
首先,很多人都在暗示你错误地使用了断言.我想很多调试专家会不同意.尽管使用断言检查不变量是一种好习惯,但断言不应限于状态不变量.事实上,许多专家调试器会告诉您断言除了检查不变量之外可能导致异常的任何条件.
例如,请考虑以下代码:
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集合添加一个实例.您可能还想提供一些简单的方法,让您的测试可以获得监听器.
注意:这对John Robbins的SUPERASSERT.NET来说非常重要.
////// TraceListener used for trapping assertion failures during unit tests. /// public class DebugAssertUnitTestTraceListener : DefaultTraceListener { ////// Defines an assertion by the method it failed in and the messages it /// provided. /// public class Assertion { ////// Gets the message provided by the assertion. /// public String Message { get; private set; } ////// Gets the detailed message provided by the assertion. /// public String DetailedMessage { get; private set; } ////// Gets the name of the method the assertion failed in. /// public String MethodName { get; private set; } ////// Creates a new Assertion definition. /// /// /// /// public Assertion(String message, String detailedMessage, String methodName) { if (methodName == null) { throw new ArgumentNullException("methodName"); } Message = message; DetailedMessage = detailedMessage; MethodName = methodName; } ////// Gets a string representation of this instance. /// ///public override string ToString() { return String.Format("Message: {0}{1}Detail: {2}{1}Method: {3}{1}", Message ?? " ", Environment.NewLine, DetailedMessage ?? " ", MethodName); } /// /// Tests this object and another object for equality. /// /// ///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; } /// /// Gets a hash code for this instance. /// Calculated as recommended at http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx /// ///public override int GetHashCode() { return MethodName.GetHashCode() ^ (DetailedMessage == null ? 0 : DetailedMessage.GetHashCode()) ^ (Message == null ? 0 : Message.GetHashCode()); } } /// /// Records the assertions that failed. /// private readonly ListassertionFailures; /// /// Gets the assertions that failed since the last call to Clear(). /// public ReadOnlyCollectionAssertionFailures { get { return new ReadOnlyCollection (assertionFailures); } } /// /// Gets the assertions that are allowed to fail. /// public ListAllowedFailures { get; private set; } /// /// Creates a new instance of this trace listener with the default name /// DebugAssertUnitTestTraceListener. /// public DebugAssertUnitTestTraceListener() : this("DebugAssertUnitTestListener") { } ////// Creates a new instance of this trace listener with the specified name. /// /// public DebugAssertUnitTestTraceListener(String name) : base() { AssertUiEnabled = false; Name = name; AllowedFailures = new List(); assertionFailures = new List (); } /// /// Records assertion failures. /// /// /// public override void Fail(string message, string detailMessage) { var failure = new Assertion(message, detailMessage, GetAssertionMethodName()); if (!AllowedFailures.Contains(failure)) { assertionFailures.Add(failure); } } ////// Records assertion failures. /// /// public override void Fail(string message) { Fail(message, null); } ////// Gets rid of any assertions that have been recorded. /// public void ClearAssertions() { assertionFailures.Clear(); } ////// 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. /// ///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(); } }
您可以在每个测试开始时将Assertions添加到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. }
恕我直言debug.asserts摇滚.这篇精彩的文章展示了如何通过向单元测试项目添加app.config并禁用对话框来阻止它们中断单元测试:
你的代码中的断言是(应该是)读者的陈述,说"这个条件在这一点上应该总是正确的".完成一些纪律,他们可以成为确保代码正确的一部分; 大多数人将它们用作调试打印语句.单元测试是一些代码,用于演示您的代码是否正确执行了特定的测试用例; 不好,他们都可以记录要求,并提高你对代码确实正确的信心.
有所作为?程序断言可以帮助您使其正确,单元测试可以帮助您培养其他人对代码正确的信心.
正如其他人所提到的,Debug断言适用于应该始终为真的事物.(这个奇特的术语是不变量).
如果您的单元测试传递的是伪造断言的伪造数据,那么您必须问自己一个问题 - 为什么会发生这种情况?
如果被测函数应该处理虚假数据,那么很明显断言不应该存在.
如果该功能没有配备处理那种数据(如断言所示),那么为什么要对它进行单元测试呢?
第二点是很多开发人员似乎陷入其中的一点.单元测试你的代码构建要处理的所有东西,并断言或抛出其他一切的异常 - 毕竟,如果你的代码不构建来处理这些情况,并导致它们发生,那么做什么呢?你期待发生什么?
您知道C/C++文档中谈论"未定义行为"的那些部分吗?就是这个.保释和保释.
更新澄清:这样做的另一面是你最终意识到你应该只Debug.Assert
用于调用其他内部事物的内部事物.如果您的代码暴露给第三方(即它是一个库或其他东西),那么您可以期望的输入没有限制,因此您应该正确验证并抛出异常或其他任何内容,您也应该对其进行单元测试