
C++函数异常处理的基础规范
在C++中,函数可以通过throw关键字主动抛出异常,也可以通过异常规格说明(C++11及之后推荐使用noexcept)明确函数是否会抛出异常,这是设计可测试函数的第一步。规范的异常处理需要遵循几个原则:首先,异常类型要清晰,尽量使用标准异常或者自定义的有明确含义的异常类,避免直接抛出基本类型;其次,异常抛出的时机要明确,只在真正出现错误无法继续执行的场景抛出,不要滥用异常做正常逻辑控制;最后,函数的异常行为要在接口注释中明确说明,方便单测编写时覆盖对应场景。
下面是一个自定义异常类和基础函数的示例:
#include <stdexcept>
#include <string>
// 自定义业务异常类,继承自标准异常
class BusinessException : public std::runtime_error {
public:
explicit BusinessException(const std::string& msg) : std::runtime_error(msg) {}
};
// 除法函数,除数为0时抛出异常
double divide(double a, double b) {
if (b == 0.0) {
throw BusinessException("除数不能为0");
}
return a / b;
}单测覆盖异常场景的必要性
很多开发者写单测时只关注正常输入场景,忽略异常场景的测试,这会导致两个问题:一是函数抛出的异常如果没有被正确捕获,可能在线上引发程序崩溃;二是如果函数的异常逻辑后续被修改,没有对应的单测就无法快速发现问题。单测覆盖异常场景的核心目标是验证两个内容:第一,函数在预期的错误输入下是否能正确抛出指定的异常;第二,异常的抛出是否会影响其他正常逻辑的执行,是否存在资源泄漏等问题。
基于常用框架编写异常单测
C++常用的单测框架比如Google Test、Catch2都支持异常测试的能力,下面以Google Test为例展示如何编写异常相关的测试用例。
Google Test异常测试示例
Google Test提供了EXPECT_THROW、ASSERT_THROW等宏来验证函数是否抛出指定类型的异常,使用方式如下:
#include <gtest/gtest.h>
#include "divide.h" // 包含上面的divide函数声明
// 测试正常场景
TEST(DivideTest, NormalCase) {
EXPECT_DOUBLE_EQ(divide(10.0, 2.0), 5.0);
}
// 测试异常场景:除数为0时抛出BusinessException
TEST(DivideTest, ZeroDivisorCase) {
EXPECT_THROW(divide(10.0, 0.0), BusinessException);
}
// 测试异常场景:验证异常消息是否正确(可选)
TEST(DivideTest, ZeroDivisorMessageCase) {
try {
divide(10.0, 0.0);
FAIL() << "预期抛出BusinessException但未抛出";
} catch (const BusinessException& e) {
EXPECT_STREQ(e.what(), "除数不能为0");
} catch (...) {
FAIL() << "抛出了非预期的异常类型";
}
}通过异常处理优化提升代码健全性
除了编写异常单测,还可以通过几个方式优化异常处理逻辑,进一步提升代码健全性:
- 对可能抛出异常的函数调用,尽量在合适的层级捕获处理,不要过度捕获所有异常,避免隐藏真正的问题
- 如果函数是
noexcept的,要确保内部不会抛出任何异常,单测中可以验证noexcept函数的行为是否符合预期 - 对于涉及资源分配的函数,抛出异常时要确保资源已经正确释放,比如使用RAII机制管理资源,避免异常导致内存泄漏
- 定期统计单测的异常场景覆盖率,确保核心函数的所有异常分支都被测试到
常见问题与注意事项
在编写异常相关的单测时,有几个常见的误区需要避免:第一,不要为了测试异常而刻意构造不合理的输入,异常场景要符合实际业务可能出现的错误情况;第二,不要在单测中忽略异常,即使预期会抛出异常,也要明确用测试框架的宏来验证,而不是直接让测试崩溃;第三,如果函数修改了异常抛出逻辑,对应的单测也要同步更新,避免单测和实际逻辑不匹配。
通过规范的异常处理设计和全面的异常单测覆盖,可以有效提升C++代码的健全性,减少线上异常引发的问题,同时也能让代码的维护成本更低。