52

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

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

这里还有一个例子,我相信它显示了困境:单元测试通过了一个受保护函数的无效输入,断言它的输入是有效的。单元测试不应该存在吗?这不是公共功能。也许检查输入会杀死性能?或者断言不应该存在?该功能受到保护而不是私有的,因此它应该检查它的输入以确保安全。

4

12 回答 12

41

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

首先,很多人建议您错误地使用断言。我认为许多调试专家会不同意。尽管用断言检查不变量是一种很好的做法,但断言不应仅限于状态不变量。事实上,除了检查不变量之外,许多专家调试器会告诉您断言任何可能导致异常的条件。

例如,考虑以下代码:

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

没关系。但是当抛出异常时,堆栈会展开,直到有东西处理异常(可能是一些顶级默认处理程序)。如果此时执行暂停(您可能在 Windows 应用程序中有一个模态异常对话框),您有机会附加一个调试器,但您可能丢失了很多可以帮助您解决问题的信息,因为大部分堆栈已展开。

现在考虑以下几点:

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

现在如果出现问题,会弹出模态断言对话框。执行立即暂停。您可以自由地附加您选择的调试器,并准确调查堆栈上的内容以及系统在确切故障点的所有状态。在发布版本中,您仍然会遇到异常。

现在我们如何处理您的单元测试?

考虑一个单元测试来测试上面包含断言的代码。您想检查当 param1 为空时是否引发了异常。您希望该特定断言失败,但任何其他断言失败都表明有问题。您希望允许特定测试的特定断言失败。

您解决此问题的方式将取决于您使用的语言等。但是,如果您使用.NET,我有一些建议(我实际上没有尝试过,但我会在未来更新帖子):

  1. 检查 Trace.Listeners。查找 DefaultTraceListener 的任何实例并将 AssertUiEnabled 设置为 false。这会阻止模式对话框弹出。您也可以清除 listeners 集合,但您不会得到任何跟踪。
  2. 编写您自己的 TraceListener 来记录断言。如何记录断言取决于您。记录失败消息可能不够好,因此您可能需要遍历堆栈以找到断言来自的方法并记录它。
  3. 测试结束后,检查发生的唯一断言失败是否是您所期望的。如果发生任何其他情况,则测试失败。

对于一个包含代码的 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.
}
于 2010-06-30T14:16:35.083 回答
18

恕我直言 debug.asserts 摇滚。这篇很棒的文章展示了如何通过将 app.config 添加到您的单元测试项目并禁用对话框来阻止他们中断您的单元测试:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.diagnostics>
    <assert assertuienabled="false"/>
</system.diagnostics>
于 2014-08-24T20:48:02.027 回答
10

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

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

  • 如果被测函数应该处理虚假数据,那么显然该断言不应该存在。
  • 如果该功能没有配备处理那种数据(如断言所示),那么您为什么要对其进行单元测试?

第二点是相当多的开发人员似乎陷入的。单元测试你的代码要处理的所有事情,并为其他所有事情断言或抛出异常 - 毕竟,如果你的代码不是为了处理这些情况而构建的,并且你导致它们发生,该怎么办你期望发生吗?
您知道 C/C++ 文档中谈论“未定义行为”的那些部分吗?就是这个。保释和保释。


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

于 2010-04-07T20:45:04.957 回答
8

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

得到区别?程序断言帮助您使其正确,单元测试帮助您建立其他人对代码正确的信心。

于 2009-01-04T00:55:41.017 回答
2

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

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

我们的 UnitTest 框架安装了自己的处理程序,用于记录消息并在断言上抛出异常。然后,UnitTest 代码将捕获这些异常(如果它们发生)并将它们记录为失败,以及断言的语句。

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

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

于 2009-01-03T22:08:19.887 回答
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 块中执行的代码(在这种情况下只有一行)将禁用其断言。

于 2020-08-07T20:39:11.053 回答
1

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

有趣的问题,因为据我了解,当您部署到生产环境时,这些断言通常会在运行时关闭。(有点违背了目的,但这是另一个问题。)

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

于 2009-01-03T22:09:29.260 回答
1

First to have both Design by Contract assertions and unit tests, your unit testing framework shall be able to catch the assertions. If your unit tests abort because of a DbC abort, then you simply cannot run them. The alternative here is to disable those assertions while running (read compiling) your unit tests.

Since you're testing non-public functions, what is the risk of having a function invoked with invalid argument ? Don't your unit tests cover that risk ? If you write your code following the TDD (Test-Driven Development) technique, they should.

If you really want/need those Dbc-type asserts in your code, then you can remove the unit tests that pass the invalid arguments to the methods having those asserts.

However, Dbc-type asserts can be useful in lower level functions (that is not directly invoked by the unit tests) when you have coarse-grained unit tests.

于 2009-01-04T17:39:01.647 回答
1

即使有单元测试,您也应该保留调试断言。

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

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

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

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

注意没有错误的问题仍然可以触发断言;例如,硬件可能会损坏。在您的问题中,您提到了集成测试;事实上,对不正确组合的集成系统进行断言是断言领域;例如,加载了不兼容的库版本。

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

于 2013-08-25T14:44:46.137 回答
0

就像其他人提到的那样,Debug.Assert陈述应该始终是真实的,即使争论不正确,断言也应该是真实的,以阻止应用程序进入无效状态等。

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

您应该无法对此进行测试(并且可能不想这样做,因为您真的无能为力!)

我可能会走得很远,但我的印象是,您可能正在谈论的断言可能更适合作为“参数例外”,例如

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

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

PK :-)

于 2010-04-07T21:08:29.033 回答
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();
    }
}
于 2017-07-20T16:18:05.203 回答
0

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

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

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

Assert 所做的另一件事是准确地指出代码中出现问题的第一个点。当您的单元测试确实发现问题时,这可以大大减少调试时间。

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

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

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

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

“只能有一个”似乎是一个合理的原则。这是常见的做法吗?还是在单元测试时禁用调试断言,以便它们可以用于集成测试?

冗余只会伤害单元测试的运行时间。如果你真的有 100% 的覆盖率,那么运行时可能是个问题。否则,不,我强烈不同意。在测试过程中自动检查你的假设并没有错。这实际上就是“测试”的定义。

这里还有一个例子,我相信它显示了困境:单元测试通过了一个受保护函数的无效输入,断言它的输入是有效的。单元测试不应该存在吗?这不是公共功能。也许检查输入会杀死性能?或者断言不应该存在?该功能受到保护而不是私有的,因此它应该检查它的输入以确保安全。

通常,单元测试框架的目的不是在违反不变假设时测试代码的行为。换句话说,如果您编写的文档说“如果您将 null 作为参数传递,则结果未定义”,您无需验证结果确实是不可预测的。如果明确定义了失败结果,则它们不是未定义的,并且 1) 它不应该是 Debug.Assert,2) 您应该准确定义结果是什么,以及 3) 测试该结果。如果您需要对内部调试断言的质量进行单元测试,那么 1) 应该检查 Andrew Grant 使断言框架成为可测试资产的方法,并且 2) 哇,您的测试覆盖率真棒!而且我认为这在很大程度上是基于项目要求的个人决定。

换句话说:Debug.Assert() 大大增加了单元测试的价值,冗余是一个特性。

于 2018-01-09T00:17:32.600 回答