单元测试期间调试断言的最佳实践

时间:2009-01-03 22:01:03

标签: unit-testing assert

大量使用单元测试是否会阻止使用调试断言?看起来在测试代码中的调试断言触发意味着单元测试不应该存在或者调试断言不应该存在。 “只有一个”似乎是一个合理的原则。这是常见做法吗?或者在单元测试时禁用调试断言,以便它们可以进行集成测试吗?

编辑:我更新了'Assert'以调试assert,以区分测试代码中的断言与测试运行后检查状态的单元测试中的行。

这里有一个我认为可以说明困境的例子: 单元测试为受保护的函数传递无效输入,该函数断言它的输入有效。单元测试不存在吗?这不是一个公共职能。也许检查输入会杀死perf?或者断言不存在?该功能不受保护,因此应检查其输入是否安全。

12 个答案:

答案 0 :(得分:36)

这是一个非常有效的问题。

首先,很多人都在暗示你错误地使用了断言。我想很多调试专家会不同意。尽管使用断言检查不变量是一种好习惯,但断言不应限于状态不变量。实际上,除了检查不变量之外,许多专家调试器会告诉您断言可能导致异常的任何条件。

例如,请考虑以下代码:

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. 一旦测试结束,请检查发生的唯一断言失败是否是您期望的失败。如果发生其他任何事情,请通过测试。
  4. 对于包含执行堆栈遍历的代码的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();
        }
    }
    

    您可以在每次测试开始时将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.
    }
    

答案 1 :(得分:12)

恕我直言debug.asserts摇滚。这个great article显示了如何通过向单元测试项目添加app.config并禁用对话框来阻止它们中断单元测试:

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

答案 2 :(得分:7)

代码中的断言是(应该是)读者的陈述,说“此时此条件应始终为真”。完成一些纪律,他们可以成为确保代码正确的一部分;大多数人将它们用作调试打印语句。单元测试是演示代码正确执行特定测试用例的代码;不好,他们都可以记录要求,并提高你对代码确实正确的信心。

有所作为?程序断言可以帮助您使其正确,单元测试可以帮助您培养其他人对代码正确的信心。

答案 3 :(得分:7)

正如其他人所提到的,Debug断言适用于应该始终为真的。 (这个奇特的术语是不变量)。

如果您的单元测试传递的是伪造断言的伪造数据,那么您必须问自己一个问题 - 为什么会发生这种情况?

  • 如果被测函数假设处理虚假数据,那么很明显断言不应该存在。
  • 如果函数配备处理那种数据(如断言所示),那么为什么要对它进行单元测试呢?

第二点是很多开发人员似乎陷入其中的问题。单元测试你的代码构建要处理的所有东西,并断言或抛出其他一切的异常 - 毕竟,如果你的代码不构建来处理这些情况,并导致它们发生,那么做什么呢?你希望发生什么? 您知道C / C ++文档中谈论“未定义行为”的那些部分吗?就是这个。保释和保释。


更新以澄清:另一方面,你最终意识到你应该只使用Debug.Assert来调用其他内部事物。 如果您的代码暴露给第三方(即它是一个库或其他东西),那么您可以期望的输入没有限制,因此您应该正确验证并抛出异常或其他任何东西,您也应该对其进行单元测试

答案 4 :(得分:2)

良好的单元测试设置将能够捕获断言。如果触发了断言,则当前测试应该失败,然后运行下一个测试。

在我们的库中,低级调试功能(如TTY / ASSERTS)具有调用的处理程序。 printf / break是默认处理程序,但客户端代码可以为不同的行为安装自定义处理程序。

我们的UnitTest框架安装自己的处理程序,用于记录消息并在断言上抛出异常。然后,如果发生这些异常,则UnitTest代码将捕获这些异常并将其作为失败记录,并与声明的语句一起记录。

您还可以在单​​元测试中包含断言测试 - 例如

CHECK_ASSERT(someList.getAt(someList.size()+ 1); //如果发生断言,则测试通过

答案 5 :(得分:2)

我采取了只在需要时禁用断言的方法,而不是在整个项目中都禁用。这是一种可以挂起断言,以免干扰测试流程的方法。

public static class TraceListenerCollectionEx
{
    /// <summary>
    /// This is a helper class that allows us to suspend asserts / all trace listeners
    /// </summary>
    public class SuspendTrackerDisposable : IDisposable
    {
        private readonly TraceListenerCollection _traceListenerCollection;
        private readonly TraceListener[] _suspendedListeners;

        public SuspendTrackerDisposable(TraceListenerCollection traceListenerCollection)
        {
            _traceListenerCollection = traceListenerCollection;

            var numListeners = traceListenerCollection.Count;
            _suspendedListeners = new TraceListener[numListeners];
            for( int index = 0; index < numListeners; index += 1 )
                _suspendedListeners[index] = traceListenerCollection[index];

            traceListenerCollection.Clear();
        }

        public void Dispose()
        {
            _traceListenerCollection.AddRange(_suspendedListeners);
        }
    }

    public static SuspendTrackerDisposable AssertSuspend(this TraceListenerCollection traceListenerCollection) => new SuspendTrackerDisposable(traceListenerCollection);
}

以下是测试中的示例用法:

    [TestMethod]
    public void EnumDefaultTest()
    {
        using(Trace.Listeners.AssertSuspend()) {
            Enum<CarClass>.DefaultValue.ShouldBe(CarClass.Unknown);  
        }
    }

在using块中执行的代码(在这种情况下只有一行)将禁用其断言。

答案 6 :(得分:1)

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

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

我会说在测试时应将它们留在代码中。您编写测试以确保正确执行前提条件。测试应该是一个“黑匣子”;在测试时,您应该充当班级的客户。如果您碰巧在生产中关闭它们,它不会使测试无效。

答案 7 :(得分:1)

首先要通过契约设计断言单元测试,您的单元测试框架应能够捕获断言。如果您的单元因DbC中止而中止测试,那么您根本无法运行它们。这里的替代方法是在运行(读取编译)单元测试时禁用这些断言。

由于您正在测试非公共函数,因此使用无效参数调用函数的风险是什么?您的单元测试不能承担这种风险吗?如果您按照TDD(测试驱动开发)技术编写代码,他们应该。

如果您确实需要/需要代码中的Dbc类型断言,那么您可以删除将无效参数传递给具有这些断言的方法的单元测试。

但是,当您进行粗粒度单元测试时,Dbc类型断言在较低级别的函数(不是由单元测试直接调用)中很有用。

答案 8 :(得分:1)

即使进行了单元测试,也应该保留调试断言。

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

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

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

在您的情况下,请编写提供错误值作为参数的单元测试。它应该期望错误返回值(或类似)。获得断言? - 重构代码以产生错误。

注意无错误的问题仍然可以触发断言;例如硬件可能会破裂。在你的问题中,你提到了集成测试;实际上,断言不正确组成的集成系统是断言领域;例如加载了不兼容的库版本。

注意,“debug”-sserts的原因是在勤奋/安全和快速/小型之间进行权衡。

答案 9 :(得分:0)

与其他人提到的一样,Debug.Assert语句应始终为 true ,即使参数不正确,断言也应为true,以阻止应用进入无效状态等。

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

你不应该测试这个(可能不想这样做,因为没有什么可以做到的!)

我可能会离开,但我得到的印象是,你可能正在谈论的断言可能更适合作为“争论例外”,例如。

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

代码中的那种“断言”仍然是非常可测试的。

PK: - )

答案 10 :(得分:0)

自问这个问题以来已经有一段时间了,但我想我有一种不同的方法可以在使用C#代码的单元测试中验证Debug.Assert()调用。注意#if DEBUG ... #endif块,这是在不在调试配置中运行时跳过测试所需的(在这种情况下,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();
    }
}

答案 11 :(得分:0)

  

大量使用单元测试是否会阻止使用调试断言?

没有。相反的。单元测试通过在运行您编写的白盒测试时仔细检查内部状态,使Debug断言更有价值。在单元测试期间启用Debug.Assert是必不可少的,因为您很少发送启用DEBUG的代码(除非性能根本不重要)。运行DEBUG代码的唯一两次是:1)执行你真正做的那么一点集成测试,抛开所有好意,以及2)运行单元测试。

使用Debug.Assert测试来编写代码很容易,在编写时检查不变量。这些检查在单元测试运行时用作健全性检查。

Assert所做的其他事情恰恰指向代码中出现问题的第一点。当您的单元测试 发现问题时,这可以大大减少调试时间。

这增加了单元测试的价值。

  

似乎在测试代码中的调试断言触发意味着单元测试不应该存在或者调试断言不应该存在。

例证。这个问题是关于真实发生的事情。对?因此,您需要在代码中使用调试断言,并且需要它们在单元测试期间触发。在单元测试期间调试断言可能触发的可能性清楚地表明应该在单元测试期间启用调试断言。

断言触发意味着您的测试正在错误地使用您的内部代码(并且应该被修复),或者一些被测试的代码正在错误地调用其他内部代码,或者某个基本假设是错误的。你没有写测试,因为你认为你的假设是错误的,你......实际上,你做了。你写测试是因为至少你的一些假设可能是错误的。在这种情况下,冗余是可以的。

  

&#34;只有一个&#34;似乎是一个合理的原则。这是常见做法吗?或者在单元测试时禁用调试断言,以便它们可以进行集成测试吗?

除了单元测试的运行时间外,冗余只会伤害你。如果确实具有100%覆盖率,则运行时可能是个问题。否则,我不强烈反对。在测试过程中自动检查您的假设没有任何问题。这实际上是&#34;测试&#34;的定义。

  

这里有一个我认为可以说明困境的例子:单元测试为受保护的函数传递无效输入,该函数断言它的输入是有效的。单元测试不存在吗?它不是公共职能。也许检查输入会杀死perf?或者断言不存在?该功能不受保护,因此应检查其输入是否安全。

通常,单元测试框架的目的不是在违反不变量假设时测试代码的行为。换句话说,如果您编写的文档说明&#34;如果您将null作为参数传递,则结果未定义&#34;,您无需验证结果确实是不可预测的。如果明确定义了失败结果,则它们不是未定义的,1)它不应该是Debug.Assert,2)你应该确切地定义结果,3)测试该结果。如果你需要对内部调试断言的质量进行单元测试,那么1)安德鲁·格兰特将断言框架作为可测试资产的方法可能应该作为答案进行检查,并且2)哇你们有很棒的测试覆盖率!我认为这主要是基于项目要求的个人决定。但我仍然认为调试断言是必不可少且有价值的。

换句话说:Debug.Assert()极大地增加了单元测试的价值,冗余是一种功能。