完善压力测试 避免系统崩溃恶果

来源互联网

 

讲到测试,人们脑海中首先浮现的是针对软件正确性的测试,即常说的功能测试。但是软件仅仅只是功能正确是不够的。

  讲到测试,人们脑海中首先浮现 的是针对软件正确性的测试,即常说的功能测试。但是软件仅仅只是功能正确是不够的。在实际开发中,还有许多其它的非功能因素在起着决定性作用。比如软件响 应速度,影响软件响应速度的因素很多,有些是因为算法不够高效,有些可能受用户并发数的影响。

  在我所负责的测试项目中,程序功能能够 满足客户需求,但当把程序交付客户使用时,由于客户网络应用环境复杂,而我们在压力测试时没有周密考虑各种可能发生的情况,软件程序在巨大负载下频繁崩 溃,使测试团队饱受客户和老板的抱怨。由此,我认识到随着网络环境的复杂性和多样性,压力测试是软件质量保证的重要元素之一,绝对不能马虎了事。

  什么是压力测试?

  在软件功能测试中,白盒和黑盒技术用于对正常程序功能和性能进行详尽的检查和测试。而压力测试(StreeTesting)则是用来对付非正常的情况。

  (1)什么是压力测试

  压力测试是指模拟巨大的工作负荷来测试应用程序在峰值情况下如何执行操作。例如模拟实际软硬件环境,在超出用户常规负荷下,长时间运行测试工具来测试被测系统的可靠性,和测试被测系统的响应时间,目的是在极限负载下识别程序的弱点。

   在众多类型的软件测试中,压力测试主要是以软件响应速度为测试目标,尤其是针对在较短时间内大量并发用户访问时软件的抗压能力。因此,压力测试是在一种 需要反常数量、频率或资源下运行系统。由于我们之前对“反常”这个关键词没有理解好,只进行了常规的测试,在这一点上客户的批评让我们感到非常汗颜,说我 们是“头发长,见识短”。

  (2)压力测试和负载测试的区别

  在这次项目测试前,我一直对压力测试和负载测试存在着一定 程度的混淆。经过这次系统崩溃后,我对压力测试和负载测试的区别有了新的认识。压力测试是在超常规负荷条件下,长时间连续运行系统,检验应用程序的各种性 能表现和反应。负载测试是指测试应用程序在常规负荷下,确认响应时间和其它的性能和表现。

  实际上,压力测试也是从比较小的负载开始,逐 渐增加模拟用户的数量,直到应用程序响应时间超时。压力测试的特点是长时间连续运行,增加超负荷(并发,循环操作,多用户)来测试什么时候系统会产生异 常,以及异常处理能力,找出瓶颈所在。现在的我终于明白到其实压力测试实际上就是超常规的负载测试。

  (3)压力测试的核心原则

  一个有效的压力测试需要遵循一些核心的基本原则,这些原则可以让我们在测试过程中时刻提醒我们压力测试是否还有更多的极端可能。

  ①重复:最明显且最容易理解的压力原则就是测试的重复。换句话说,重复测试就是一遍又一遍地执行某个操作或功能。功能测试是验证一个操作能否正常执行,而压力测试则是确定一个操作能否在长时间内每次执行时都正常。

  ②并发:并发是同时执行多个操作的行为。换句话说,就是在同一时间执行多个测试用例。功能测试或单元测试几乎不会与任何并发设计结合。因此,压力系统必须超越功能测试,要同时遍历多条代码路径。

   ③量级:压力测试另一个重要原则就是要给每个操作增加超常规的负载量。就是说压力测试可以重复执行一个操作,但是在操作自身过程中也要尽量给程序增加负 担,增加操作的量级。一般来说,单独的高强度操作重复自身可能发现不了代码错误,但与其他压力测试方法(如并发和量级)结合在一起时,将可以增加发现错误 的机会。

  ④随机:意思是任何压力测试都应该多多少少具有一些随机性。例如随机组合前面三种压力测试原则,然后变化出无数种测试形式,就 能够在每次测试运行时应用许多不同的代码路径来进行压力测试。当一个压力测试结合的原则越多,测试执行的时间越长,就可以遍历越多的代码路径,发现的错误 也会越多。

  压力测试对系统的重要作用

  我们对应用程序进行压力测试时经常会出现这种情况,就是测试到了最后却发现不明白测试结果有什么意义?实际上,当我们都不明白压力测试的意义时,我们就不能设计出各种极限测试用例。

  压力测试不同于功能测试,软件的正确性并不是它的测试重点,它所看重的是软件的执行效率,尤其是短时间内访问用户数爆炸性增长时软件的响应速度。因此,明白压力测试的作用,对我们高效完成压力测试有至关重要的指导意义。

  (1)测试应用程序的可靠性

   在系统崩溃后总结之前失败的压力测试时,我忽视的第一个要点就是没有测试出应用程序在压力下的可靠性。压力测试除了对每个单独的组件进行压力测试外,更 应该对带有其所有组件和支持服务的整个应用程序进行集中压力测试,以检查在巨大的工作负荷时,应用程序在峰值情况下是否可靠的执行操作。例如,当实际情况 是平均每秒出现1个或2个中断的情形下,应当对每秒出现10个中断的情形来进行特殊的测试;又或者把输入数据的量提高一个数量级来测试输入功能是否可靠的 响应。从本质上来说,压力测试是想要看在最大极限时程序是否可靠的运行。

  (2)测试应用程序的并发性能

  进行压力测试需要对实际的并发访问量有一个正确的预期估算,否则在负载远远大于事前预测的压力下系统将脆弱得不堪一击。导致系统崩溃的因素有很多,处理能力、存储速度、响应时间、网络带宽等无论哪部分出现短板拥堵、后果都可能导致全盘崩溃。

  现在我明白,哪怕硬件条件达到了,如果软件的并行处理能力不足将会导致等候队列过长,响应时间变慢,系统崩溃也只是时间问题。简单说就是:压力测试是考察当前软硬件环境下系统所能承受的最大并发负荷,并帮助找出软件程序的瓶颈所在。

  (3)测试应用程序的最大负载能力

   压力测试的目的之一是找出应用程序能够支持的最大客户端数。通过多次的运行和对测试结果中正在运行用户数与错误用户的对比,然后根据可接受错误率就可得 到该功能的最大负载访问的用户数。最大负载压力测试用来评估在超越最大负载的情况下系统将如何运行,这时的目标是要发现在高负载的条件下应用程序的缺陷 (Bug),例如内存泄漏等。因此,最大负载能力不但是应用程序一个重要的技术指标,也是客户评估和验收软件的一个关键指标。

  如何进行高效的压力测试?

  软件测试有两句通俗的话:开发是尽可能地让程序通过;而测试则是尽可能地让程序通不过。对于压力测试而言,测试效果好不好,测试计划的好坏是关键。所以,针对不同的情况,分析后有针对的进行测试,比起拿枪乱打、无的放矢显然要高效得多。

   进行一次切实可行的压力测试并不像乍看之下那么简单,遇到的问题也可能非常微妙。例如,我的测试团队就经常遇到诸如“客户端每小时将要处理100个客户 订单请求”等此类的需求,于是测试团队就试图把该需求转化为某种测试需求,执行这种测试需求的常见方法就是以死循环的形式对服务器进行反复请求,然后静观 其效。然而,通常事情进行得并不顺利,原因在于这只是把需求表面化了,没有分析出测试需求的本质。高效的压力测试应遵循以下这几个步骤:

  (1)确定测试目标

   在确定压力测试目标中,我们要定义测试的对象,并对每一个测试对象给出清晰说明,也要定义测试结束的目标。为控制测试的有效性以及完成程度,必须定义准 则和策略。准则必须是客观的,可量化的,而不能是经验或感觉。例如压力测试目标可能是测定终端用户处理事务的响应时间,它可能随用户的增加而增加,但要定 义一个可接受时间。在确定压力测试目标过程中,最好能邀请客户、设计人员等一同对测试目标进行评审。

  (2)制定压力测试计划

   测试计划内容包括:定义测试资源、制定测试进度表、选择测试工具等。制定测试计划的目的是使压力测试有章可循并得到人力、物力等各方面的保证;在制定测 试进度表时应考虑和开发进度相互协调;对于测试工具的选择应以满足测试目标为前提。所以,这并不是说测试工具提供的功能越多就越好,在实际的选择过程中适 用才是根本。

  (3)编写测试案例和设置测试数据

  测试人员一般是根据测试案例进行实际的测试工作,因此测试案例的编写 应做到客观全面、重点突出,也就是要求编写的测试案例应该尽可能模拟真实的负荷,不遗漏重要的测试内容。为了让所有的测试顺利执行,可采取数据驱动方式进 行,同时应该对测试数据进行参数化。另外,一般不提倡在开发环境中进行压力测试,最好是另外构建测试环境。

  (4)结果分析及测试报告

   压力测试运行结束后,应把所有的数据汇总并记录到文件中,以方便对测试结果进行分析和得出结论。若测试失败,应先分析失败原因,如果是软件系统造成的, 应返回给设计人员修改。如果测试结果不满足预期需求,应先对软件程序进行优化调理,然后再次运行测试,直到可以满足预期需求或调整已无法改善结果。

  最后需要注意的是测试报告。报告应包括测试提要、测试环境和测试结果。提要应简单说明测试方法、策略、范围、内容;测试环境应包括资源开销、环境配置等;测试结果必须包括测试是否通过或拒绝,并要对测试结论进行说明,并对软件程序的性能做出评价。

<think> 我们首先明确问题:在C语言宏定义中,使用do{...}while(0)形式时,如果在宏定义的while(0)后面加上分号,会产生什么不良后果?并举例说明。 ### 分析 宏定义中,我们通常使用`do{...}while(0)`来包裹多条语句,这样在宏展开后可以像一个单独的语句一样使用。例如: ```c #define MY_MACRO() do { \ func1(); \ func2(); \ } while(0) // 注意:这里没有分号 ``` 然后我们调用这个宏时,会加上分号: ```c MY_MACRO(); // 这里的分号是调用时加的,与宏定义无关 ``` 但是,如果在宏定义中`while(0)`后面加上分号,即: ```c #define MY_MACRO() do { \ func1(); \ func2(); \ } while(0); // 错误:这里多了一个分号 ``` 那么当我们在某些上下文中调用这个宏时,就会产生问题。 ### 不良后果及示例 主要问题在于:宏展开后会多出一个分号,这个分号可能会导致语法错误,尤其是在条件语句(如if-else)中。 #### 示例1:在if语句中不带else时 ```c if (condition) MY_MACRO(); else do_something(); ``` 展开后: ```c if (condition) do { func1(); func2(); } while(0);; // 注意:这里有两个分号(一个来自宏定义,一个来自调用处) else do_something(); ``` 这里,宏展开后,在`while(0)`后面有两个分号。第一个分号是宏定义中自带的,第二个分号是调用时加上的。这样在`if`分支中,实际上有两个语句:一个是`do-while`循环语句(以分号结束),另一个是空语句(由第二个分号构成)。这样在语法上是正确的,但并没有实际错误。 #### 示例2:在if语句中,且后面有else时(问题所在) ```c if (condition) MY_MACRO(); else do_something(); ``` 展开后: ```c if (condition) do { func1(); func2(); } while(0);; // 这里有两个分号 else // 这个else将没有对应的if,因为上面的if分支已经结束 do_something(); ``` 实际上,上面的代码在语法上是错误的。因为`if`分支有两个语句:`do-while`语句和空语句。所以`if`分支结束后,后面跟了一个`else`,而`else`之前必须是一个完整的`if`语句,但这里`if`已经结束了(因为有两个语句,用分号隔开,所以`if`的作用范围只到第一个分号)。因此,`else`没有匹配的`if`,导致编译错误。 ### 详细解释 - 在宏定义中,我们期望的是宏展开后是一个单独的语句。所以我们在调用时加上分号,形成一个完整的语句。 - 但是,如果宏定义内部已经包含了一个分号(即在`while(0)`后面加了分号),那么宏展开后就会有两个分号:一个来自宏定义内部,一个来自调用处。这样就会形成两个语句:第一个是`do-while`循环,第二个是一个空语句(只有一个分号)。 - 在条件语句中,如果`if`后面跟了一个复合语句(用花括号括起来的多个语句)或者多个语句,那么`else`必须紧跟在该`if`之后,中间不能插入其他语句。而这里,由于宏展开后有两个语句,所以在`if`分支中,第一个语句(`do-while`)结束后,第二个语句(空语句)就结束了`if`分支,然后`else`就失去了匹配的`if`。 ### 正确做法 在宏定义中,`do{...}while(0)`后面不应该加分号,这样宏展开后,调用处加上的分号就正好成为整个语句的结束符。 例如,正确的宏定义: ```c #define MY_MACRO() do { \ func1(); \ func2(); \ } while(0) // 没有分号 ``` 调用: ```c if (condition) MY_MACRO(); // 展开后:do{...}while(0); 只有一个分号 else do_something(); ``` 展开后: ```c if (condition) do { func1(); func2(); } while(0); // 这里只有一个分号,是调用时加上的 else do_something(); ``` 这样,整个`if`分支就是一个单独的`do-while`语句,后面跟着的`else`正确匹配。 ### 结论 在宏定义中,`do{...}while(0)`后面加上分号会导致宏展开后在某些情况下(特别是`if-else`语句中)出现多余的分号,从而造成语法错误。因此,我们应该避免在宏定义的`while(0)`后面加分号,而在调用宏的时候加上分号。 ### 引用参考 在引用[1]中提到:宏扩展后将变成: ```c if (!feral) { bar(wolf); baz(wolf); }; else bin(wolf); ``` 大家可以看出,这就有语法错误了[^1]。 引用[2]中的例子也展示了类似的问题,在`if(false)`后面使用宏,如果宏定义不正确,会导致后续的语句不受`if`控制[^2]。 因此,务必注意宏定义中`do{...}while(0)`后面不要加分号。 ### 相关问题 1. 在宏定义中,除了`do{...}while(0)`,还有哪些方法可以安全地包含多条语句? 2. 在宏定义中如何避免参数多次求值? 3. 宏定义和内联函数有什么区别?在什么情况下应该使用宏,什么情况下使用内联函数? [^1]: do {...} while (0) 在宏定义中的作用 [^2]: C++ do{...}while(0)的巧妙用法 </think> ### 在C语言宏定义中 `do{...}while(0)` 后加分号的不良后果及示例 在C语言宏定义中,`do{...}while(0)` 后**加上分号会导致语法错误或逻辑错误**,主要原因是在宏展开时会产生多余的分号,破坏代码结构。以下是具体分析和示例: --- ### 一、问题核心:多余分号导致语法错误 #### 错误宏定义示例 ```c // 错误!while(0)后加了分号 #define UNSAFE_MACRO() do { \ printf("Step1\n"); \ printf("Step2\n"); \ } while(0); // ← 此处错误加分号 ``` #### 正确宏定义 ```c // 正确:while(0)后无分号 #define SAFE_MACRO() do { \ printf("Step1\n"); \ printf("Step2\n"); \ } while(0) // ← 无分号 ``` --- ### 二、具体不良后果及示例 #### 场景1:`if-else` 语句中断裂(最常见问题) **错误调用代码**: ```c if (condition) UNSAFE_MACRO(); // 调用时正常加分号 else printf("Error"); ``` **宏展开结果**: ```c if (condition) do { printf("Step1\n"); printf("Step2\n"); } while(0);; // 两个连续分号! else // ← else失去匹配的if,编译错误 printf("Error"); ``` **问题分析**: 1. 宏定义中的 `;` 和调用时的 `;` 形成两个连续分号 `;;` 2. 第二个分号被视为空语句,导致 `if` 语句提前结束 3. `else` 无法匹配到任何 `if`,产生编译错误 --- #### 场景2:循环控制失效 **错误调用代码**: ```c while (condition) UNSAFE_MACRO(); // 希望每次循环执行宏 ``` **宏展开结果**: ```c while (condition) do { // ... } while(0);; // 两个分号 ``` **问题分析**: 1. 虽然不会直接编译失败 2. 但多余的空语句 `;` 可能干扰调试 3. 某些静态分析工具会报告"多余空语句"警告 --- #### 场景3:代码块作用域混乱 **错误调用代码**: ```c #define UNSAFE_MACRO() do { int x=0; ... } while(0); int main() { if (flag) UNSAFE_MACRO(); // 展开后有两个分号 return 0; } ``` **宏展开结果**: ```c if (flag) do { int x=0; ... } while(0);; // 两个分号 ``` **问题分析**: 1. 第二个分号形成独立空语句 2. 在 `-Wpedantic` 编译模式下会触发警告 3. 破坏代码的视觉一致性 --- ### 三、正确使用规范 #### 定义时(无分号) ```c #define SAFE_MACRO() do { \ statement1; \ statement2; \ } while(0) // ← 无分号 ``` #### 调用时(必须加分号) ```c SAFE_MACRO(); // ← 此处加分号 if (x) { SAFE_MACRO(); // 正确 } else { ... } ``` --- ### 四、为什么设计如此? 1. **语法一致性** `do{...}while(0)` 本身是一个完整表达式,需要调用时的 `;` 形成语句结束符 2. **复合语句兼容** 确保宏在以下场景行为一致: ```c // 场景A:独立语句 SAFE_MACRO(); // 展开为 do{...}while(0); // 场景B:if分支 if (x) SAFE_MACRO(); // 展开为 if (x) do{...}while(0); ``` 3. **空语句防护** 避免出现危险的独立分号: ```c if (error) UNSAFE_MACRO() // 用户忘记分号时会更早暴露错误 ``` --- ### 总结 | **情况** | 宏定义 `while(0)` 后 | 调用时 | 结果 | |---------------------|----------------------|--------|------------| | **正确用法** | **不加分号** | 加分号 | 无错误 | | **危险用法** | 加分号 | 加分号 | 语法错误 | | **未完成语句** | 不加分号 | 不加号 | 语法错误 | **根本原则**: 宏定义中的 `do{...}while(0)` 本质是创建一个**表达式**,而调用时的分号才是**语句终结符**。两者配合才能在各种上下文中保持语法正确性[^1][^2]。 --- ### 相关问题 1. 除了 `do{...}while(0)`,宏定义中还有哪些安全封装多语句的方法? 2. 在宏定义中如何避免参数被多次求值? 3. 为什么有些代码库使用 `({...})` 代替 `do{...}while(0)`?它有何优缺点? [^1]: do {...} while (0) 在宏定义中的作用 [^2]: C++ do{...}while(0)的巧妙
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值