Skip to main content
 首页 » 编程设计

unit-testing之单元测试期间调试断言的最佳实践

2024年06月03日41www_RR

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

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

还有一个我认为可以说明困境的例子: 单元测试传递 protected 函数的无效输入,该函数断言其输入有效。单元测试不应该存在吗?这不是公共(public)职能。也许检查输入会杀死性能?或者断言不应该存在?该函数受到非私有(private)保护,因此应该检查其输入以确保安全。

请您参考如下方法:

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

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

例如,考虑以下代码:

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. 测试结束后,检查唯一发生的断言失败是否是您所期望的。如果发生任何其他情况,则测试失败。

对于包含执行此类堆栈遍历的代码的 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. 
}