65.9K
CodeProject 正在变化。 阅读更多。
Home

调试和测试变得容易(第一部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.21/5 (14投票s)

2003年3月10日

4分钟阅读

viewsIcon

75689

downloadIcon

522

一些宏和技巧,可以减少 bug 的出现,并使用类使单元测试轻松而简单

引言

您是否经常发现自己在调试代码,一切看起来都合乎逻辑,但就是不起作用?初学者经常会遇到这种情况。嗯,我最近发现这种情况少了很多,这得益于我学到的一些很棒的技巧。我想和大家分享,并希望它们也能帮助您。

背景

我用 VC++ 编程,但经常发现自己为各种事情编写控制台实用程序。VC++ 和 MFC 有一些很棒的宏(C++ #define)可以帮助您调试。但如果您在项目中使用的是非 MFC,那您就陷入困境了。不过,那些宏并没有什么神奇之处,并且很容易为您的非 MFC 项目(甚至非 VC++ 项目)重现。

在我看来,代码中存在 bug 的最主要原因之一是,对特定代码段应该(和不应该)做什么的假设是错误的。所以解决办法是在代码中记录您的假设,并让您的代码在不按预期工作时,或者在使用(或误用)不当的方式时给出警告。

一些理论(我希望它不会枯燥)- Bertrand Meyer 引入了“契约式设计”的概念,正是为了做到这一点(以及更多,但我们不想在本文中深入探讨这些细节)。但是 C++ 并不像 Eiffel 那样支持契约。那么我们该怎么办呢?我们尝试找出实现契约的方法。

我说的这个契约到底是什么?我不需要什么法律术语来编程。您也不需要。基本上,函数的(或方法的)契约表明了它为了正确工作而期望满足的条件,调用者施加给它的条件,最后,还有一些不应被违反的全局条件。

函数为正确工作而必须满足的条件是它的前置条件或“REQUIRE”条件。例如,“参数不应为 null”是一个非常常见的要求。调用者施加的特定函数的条件必须得到满足。这是为了“ENSURE”结果符合预期。例如,如果函数涉及文件操作,则操作确实已完成,未被中止。全局条件通常称为不变式或“ASSERT”断言。

在调试或开发时,我们希望在契约被违反时收到通知。因为当契约被违反时,它表明结果不一定正确,有问题。最简单的方法之一是将消息打印到控制台,例如“前置条件不满足”、“断言失败”或“某个变量的值为 X,而该断言失败”。嗯,这里有一些宏可以做到这一点。

#define DBGOUT cout //you could just as easily put 
                    //name of a ofstream here and log everything to a file.

#define REQUIRE(cond) \
if (!(cond))\
{\
    DBGOUT << "\nPrecondition  \t" << #cond << "\tFAILED\t\t" \
           << __FILE__ << "(" << __LINE__ << ")";\
};\

//precondition tracing macro

#define ENSURE(cond)\
if (!(cond)) \
{\
    DBGOUT << "\nPostcondition \t" << #cond << "\tFAILED\t\t" \
           << __FILE__ << "(" << __LINE__ << ")";\
};\

//postcondition tracing macro

#define ASSERT(cond) \
if (!(cond)) \
{\
    DBGOUT << "\nAssertion     \t" << #cond << "\tFAILED\t\t" \
           << __FILE__ << "(" << __LINE__ << ")";\
};\

//invariant tracing macro

#define TRACE(data) \
    DBGOUT << "\nTrace         \t" << #data << " : " << data \
           << "\t\t" << __FILE__ << "(" << __LINE__ << ")";

//dump some variables value.

#define WARN(str) \
    DBGOUT << "\nWarning       \t" << #str << "\t\t" \
           << __FILE__ << "(" << __LINE__ << ")";

//print some warning indicating that some code is being executed which 
//shouldn't really be executed.

Using the Code

让我们以一个除以两个数的函数为例,看看这些宏会有什么帮助。

int div (int a, int b)
{
  //preconditions
  REQUIRE(b != 0)

  int result = a/b;

  //postconditions
  ENSURE(b*result <= a)

  return a/b;
}

上面的例子看起来可能很简单,但考虑更复杂的函数,并正确使用上述宏,您至少可以找出最常见的错误原因。TRACEWARN 宏在弄清楚为什么某事物不符合预期时特别有用,而 ENSUREREQUIREASSERT 将让您确信,如果您的函数获得正确的输入,它们将产生正确的输出。

稍微复杂一点的例子

//to parse some string in numbers (thousand) to its equivalent integer(1000)
int Parse(char* str)
{
  //preconditions
  REQUIRE(str != NULL) //null strings cannot be parsed
  int result;

  //some table lookup is done here
  //assert that index is within array bounds
  ASSERT (lookupindex < tablesize)

  //postconditions
  ENSURE(result >= 0) // the caller doesnt expect negative answers
  return result;
}

误用

  1. 这些宏有一些缺点和误用。首先,它们会减慢处理速度。所以,您通常不希望它们出现在发布代码中,只在开发或调试时使用。在这种情况下,您可以有条件地定义宏,使其在调试时生成代码,但在发布时则不生成(请参阅 zip 文件,它在 dbgmacros.h 中是这样定义的)。
  2. 其次,宏期望测试不变条件,并且不期望在其中进行任何处理。例如,ASSERT(i++!=10) - 这段代码在您的发布版本中最有可能失败。请不要在宏中进行处理。只检查条件。

关注点

为了更深入地了解,我强烈建议您深入研究契约式设计的理念。它对面向对象系统的设计和实现有着巨大的影响。

我将在本系列的下一部分中提供类似的宏,以简化您的测试,可能在下周。

历史

  • 2003年3月10日:第一次修订

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.