debugging的最佳实践在unit testing期间声明

大量使用unit testing是否会阻止使用debugging断言? 这似乎是一个debugging断言在testing中的代码意味着unit testing不应该存在或debugging断言不应该存在。 “只能有一个”似乎是一个合理的原则。 这是常见的做法吗? 还是你在unit testing时禁用debugging断言,所以他们可以在集成testing?

编辑:我更新了'断言'debugging断言来区分在testing代码中的断言和unit testing中的行,在testing运行后检查状态。

另外这里是一个我相信显示困境的例子:一个unit testing通过一个被保护函数的无效input,声明它的input是有效的。 unit testing是否应该存在? 这不是公共职能。 也许检查input会杀死perf? 还是应该断言不存在? 该function被保护不是私人的,所以它应该检查它的input是否安全。

这是一个完全有效的问题。

首先,很多人都build议你错误地使用断言。 我想很多debugging专家会不同意。 尽pipe检查不变式的断言是一个很好的习惯,断言不应该局限于状态不variables。 事实上,许多专家debugging器会告诉你除了检查不变式之外,还可以声明任何可能导致exception的条件。

例如,请考虑以下代码:

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

没关系。 但是当引发exception时,堆栈会解开,直到处理exception(可能是一些顶级的默认处理程序)。 如果执行暂停(在Windows应用程序中可能有一个模态exception对话框),那么您有机会附加一个debugging器,但是您可能已经失去了很多可以帮助您解决问题的信息,因为大部分堆栈已经被解开。

现在考虑以下几点:

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

现在,如果发生问题,popup模式断言对话框。 执行暂时暂停。 您可以自由地连接您select的debugging器,准确调查堆栈上的内容以及系统在确切故障点的所有状态。 在发布版本中,你仍然会得到一个exception。

现在我们该如何处理你的unit testing?

考虑一个unit testing,testing上面包含断言的代码。 当param1为空时,您想检查是否引发exception。 你期望这个特定的断言失败,但是任何其他的断言失败都会表明有什么错误。 您希望允许特定的testing失败。

你解决这个问题的方式将取决于你正在使用的语言等。 不过,如果您使用.NET,我有一些build议(我没有真的尝试过,但我将来会更新这个post):

  1. 检查Trace.Listeners。 查找DefaultTraceListener的任何实例,并将AssertUiEnabled设置为false。 这会阻止popup的模式对话框。 你也可以清除收听者的collections,但是你什么也得不到。
  2. 编写自己的TraceListener来logging断言。 你如何logging断言取决于你。 logging失败消息可能不够好,所以你可能需要走堆栈以find断言来自的方法并logging下来。
  3. 一旦testing结束,检查发生的唯一的断言失败是你所期待的。 如果有其他人发生,则通过testing。

对于一个TraceListener的例子,其中包含代码来做这样的栈走,我会searchSUPERASSERT.NET的SuperAssertListener并检查它的代码。 (如果你对使用断言进行debugging真的很认真,那么也应该集成SUPERASSERT.NET)。

大多数unit testing框架支持testing设置/拆卸方法。 您可能需要添加代码来重置跟踪侦听器,并声明在这些区域中没有任何意外的断言失败,以最大限度地减less重复和防止错误。

更新:

这里是一个TraceListener的例子,可以用来unit testing断言。 您应该将一个实例添加到Trace.Listeners集合中。 您可能还想提供一些简单的方法,让您的testing可以抓住侦听器。

注意:这对于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(); } } 

您可以在每个testing开始时,将断言添加到AllowedFailures集合中,以获得您期望的断言。

在每个testing结束时(希望你的unit testing框架支持一个testing拆解方法):

 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. } 

正如其他人所说,debugging断言是为了应该永远是真实的东西。 (对此的奇特术语是不变式 )。

如果你的unit testing是通过假设断言的假数据,那么你不得不问自己的问题 – 为什么发生这种情况?

  • 如果被testing的function应该处理伪造的数据,那么显然该断言不应该存在。
  • 如果这个函数没有配备处理这种types的数据(如assert指出的那样),那么为什么你要对它进行unit testing呢?

第二点是很多开发者似乎都陷入了困境。 unit testing你的代码所处理的所有东西,并断言或抛出所有其他的exception – 毕竟,如果你的代码不是为了处理这些情况而build立的,你期望发生?
你知道谈论“未定义的行为”的C / C ++文档的那些部分? 就是这个。 保释和保重。

恕我直言debug.asserts摇滚。 这篇伟大的文章展示了如何通过将app.config添加到unit testing项目中来阻止他们中断unit testing,并禁用对话框:

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

你的代码中的断言是(应该是)给读者的陈述,说“这个条件在这一点上总是应该是真的”。 做一些纪律,他们可以是确保代码是正确的一部分; 大多数人使用它们作为debugging打印语句。 unit testing是代码, certificate您的代码正确执行特定的testing用例; 不好,他们都可以logging这些要求,并提高你的信心,代码确实是正确的。

获得差异? 程序断言帮助你使它正确,unit testing帮助你发展别人对代码是正确的信心。

你应该保持你的debugging断言,即使在unit testing的地方。

这里的问题不是区分错误和问题。

如果一个函数检查其错误的参数,它不应该导致debugging断言。 相反,它应该返回一个错误值。 用错误的参数调用函数是一个错误。

如果一个函数传递了正确的数据,但由于运行时内存不足而无法正常运行,那么由于这个问题,代码应该发出debugging断言。 这是一个基本假设的例子,如果它们不成立,那么“所有的赌注都是失败的”,所以你必须终止。

在你的情况下,编写提供错误值作为参数的unit testing。 它应该期望一个错误返回值(或类似的)。 获取断言? – 重构代码来产生一个错误。

注意一个无bug的问题仍然可以触发断言; 例如硬件可能会中断。 在你的问题中,你提到了集成testing; 的确,对不正确组成的综合系统的主张是断言的领土; 例如加载的不兼容的库版本。

请注意,“debugging”声明的原因是勤奋/安全和快/小之间的折衷。

一个好的unit testing设置将有能力捕捉断言。 如果一个断言被触发,那么当前的testing将会失败,并且下一个将会运行。

在我们的库中,像TTY / ASSERTS这样的低级别的debuggingfunction具有被调用的处理程序。 默认处理程序将printf / break,但客户端代码可以安装自定义处理程序的不同行为。

我们的UnitTest框架会安装自己的处理程序来logging消息并在断言上抛出exception。 然后,UnitTest代码将捕获这些exception,如果它们发生并logging为失败,并声明声明。

你也可以在你的unit testing中包含断言testing – 例如

CHECK_ASSERT(someList.getAt(someList.size()+ 1); //testing通过,如果一个断言发生

你的意思是C / Java断言“合同编程”断言,或CppUnit / JUnit断言? 最后一个问题让我相信它是前者。

有趣的问题,因为我的理解是,这些断言在部署到生产环境时经常在运行时closures。 (基达击败了目的,但这是另一个问题。)

我会说,当你testing它们时,它们应该留在你的代码中。 你写testing,以确保前提条件正确执行。 testing应该是一个“黑匣子”; 你应该在考试时充当class上的客户。 如果您在生产中碰巧closures它,则不会使testing失效。

首先要有合同devise声明unit testing,你的unit testing框架应该能够抓住主张。 如果你的unit testing由于DbC中止而中止,那么你根本无法运行它们。 这里的select是在运行(读编译)unit testing的时候禁用这些断言。

由于您正在testing非公共函数,因此使用无效参数调用函数的风险是什么? 你的unit testing不包括这个风险吗? 如果你按照TDD(testing驱动开发)技术编写你的代码,他们应该这样做。

如果你真的想在你的代码中使用那些Dbctypes的断言,那么你可以将传递了无效参数的unit testing移除到具有这些断言的方法中。

但是,当您进行粗粒度的unit testing时,Dbctypes的断言在较低级别的函数中可能是有用的(不是由unit testing直接调用的)。

像其他人提到的一样, Debug.Assert语句应该总是正确的 ,即使参数不正确,断言应该是真的,以阻止应用程序进入无效状态。

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

你不应该能够testing这个(可能不想要,因为没有什么可以做的!)

我可能会离开,但我的印象是,也许你所说的断言更适合作为“论证例外”,例如

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

你的代码中的这种“断言”仍然是非常可testing的。

PK 🙂

这个问题已经有一段时间了,但是我想我有一种不同的方法来validation使用C#代码的unit testing中的Debug.Assert()调用。 请注意#if DEBUG ... #endif块,在不在debuggingconfiguration中运行时跳过testing(在这种情况下,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(); } }