专业手机移动网站设计,php网站调试环境搭建,网络优化推广公司哪家好,深圳企业网站制作哪个INI Parser 测试编写完整指南 - 从零开始
前言
很多朋友基本上写完工程直接就跑了#xff0c;的确#xff0c;在之前我们编写了伪测试#xff0c;对着他把我们的代码写完了#xff0c;但是能不能过测试#xff0c;这个才是向其他人表示咱们的代码是靠谱的根本手段
测试…INI Parser 测试编写完整指南 - 从零开始前言很多朋友基本上写完工程直接就跑了的确在之前我们编写了伪测试对着他把我们的代码写完了但是能不能过测试这个才是向其他人表示咱们的代码是靠谱的根本手段测试的基本概念搭建测试框架编写第一个测试添加更多测试场景完善测试套件测试的基本概念先来点废话看笔者这个教程的朋友大多应该没有什么工程概念真的很有必要跟你说一说测试是什么。测试一个好理解的比喻就是给你的代码做体检。我们压根不关心里面你写的啥史山不关心关心的是你的代码行为产出是否具备通用性。所以我们就像用户一样——搞定三个步骤喂给你的系统数据然后执行然后看对不对简单搭建测试框架其实我们大可以直接使用catch2啊等等一系列已经搞定的C/C测试框架但是笔者想了想还是手搓一个简单的测试框架先理解这里面的门道之后使用catch2干活更利索。让我们从最简单的开始。创建一个新文件test_ini_parser.cpp#includeini_parse.h#includeiostreamintmain(){std::cout开始测试...std::endl;return0;}现在编译运行一下确保能正常工作我们需要一些工具来帮助测试#includeini_parse.h#includecassert// 用于断言#includeiostream// 用于输出#includestring// 用于字符串处理usingnamespacecxx_utils::ini_parser; 小贴士using namespace让我们可以直接写IniParser而不用写cxx_utils::ini_parser::IniParser毕竟我们的测试还是专一的这地方上梭哈完整的namespace多少没必要了此外我们需要知道通过了多少测试失败了多少// 在 main 函数之前添加inttests_passed0;// 通过的测试数inttests_failed0;// 失败的测试数创建测试宏 - TEST_CASE这个宏让每个测试都有一个漂亮的标题#defineTEST_CASE(name)\std::cout\n name std::endl;我们往里面嘎嘎写就有了TEST_CASE(基本解析测试);// 输出 基本解析测试 TEST_CASE更加实际的作用是将测试分组了这里表示的就是一组话题相关的测试比如说我们很快要搞的一组测试是否正确处理了空白字符等等的组测试。创建断言宏 - ASSERT_EQ这是测试的核心用来检查实际值是否等于期望值#defineASSERT_EQ(actual,expected,msg)\do{\if((actual)(expected)){\tests_passed;\std::cout✓ msgstd::endl;\}else{\tests_failed;\std::cout✗ msg\n Expected: (expected)\\n Got: (actual)std::endl;\}\}while(0)让我们拆解一下这个宏do { ... } while(0)这是个常见技巧确保宏像一条语句一样工作条件判断比较 actual 和 expected成功时计数器1显示 ✓失败时计数器1显示 ✗ 和详细信息ASSERT_EQ(22,4,2加2应该等于4);// 输出✓ 2加2应该等于4添加 ASSERT_TRUE 和 ASSERT_FALSE有时我们只需要检查条件是真还是假#defineASSERT_TRUE(condition,msg)\do{\if(condition){\tests_passed;\std::cout✓ msgstd::endl;\}else{\tests_failed;\std::cout✗ msg (条件失败)std::endl;\}\}while(0)#defineASSERT_FALSE(condition,msg)\do{\if(!(condition)){\tests_passed;\std::cout✓ msgstd::endl;\}else{\tests_failed;\std::cout✗ msg (期望为假)std::endl;\}\}while(0)到这里对于我们的ini parser而言够了。编写第一个最简单的测试 - test_basic_parsing让我们从最基础的测试开始。先写一个空的函数框架voidtest_basic_parsing(){// 这里将会是我们的测试代码}1.1 添加测试标题首先我们给这个测试一个好看的标题这样运行时可以看到在测试什么voidtest_basic_parsing(){TEST_CASE(Basic Parsing);}运行后会显示 Basic Parsing 1.2 准备测试数据现在我们需要一些 INI 数据来测试。我们使用 C11 的原始字符串字面量voidtest_basic_parsing(){TEST_CASE(Basic Parsing);constchar*iniRini( key1 value1 key2 value2 )ini;}解释Rini( ... )ini是原始字符串可以包含多行不需要转义ini是分隔符你也可以用其他名字比如R***( ... )***为什么用这个因为 INI 文件是多行的用普通字符串会很麻烦1.3 创建解析器并解析接下来我们创建一个解析器对象并让它解析我们的数据voidtest_basic_parsing(){TEST_CASE(Basic Parsing);constchar*iniRini( key1 value1 key2 value2 )ini;IniParser parser;// 创建解析器boolokparser.parse(ini);// 解析数据返回是否成功}现在的状态数据已经被解析了但我们还没有验证结果1.4 验证解析是否成功我们的第一个断言确保解析成功了voidtest_basic_parsing(){TEST_CASE(Basic Parsing);constchar*iniRini( key1 value1 key2 value2 )ini;IniParser parser;boolokparser.parse(ini);ASSERT_TRUE(ok,Parse should succeed);}解释ASSERT_TRUE检查ok是否为 true第二个参数是描述信息如果成功会显示✓ Parse should succeed如果失败会显示✗ Parse should succeed (condition failed)1.5 验证 key1 的值现在我们要检查解析出来的值对不对。先检查 key1voidtest_basic_parsing(){TEST_CASE(Basic Parsing);constchar*iniRini( key1 value1 key2 value2 )ini;IniParser parser;boolokparser.parse(ini);ASSERT_TRUE(ok,Parse should succeed);ASSERT_EQ(parser.get(,key1).value_or(),value1,Top-level key1);}让我们拆解这一行parser.get(,key1)// 获取值返回 std::optionalstd::string.value_or()// 如果没有值返回空字符串参数说明第一个是 section 名空字符串表示顶层没有 section第二个key1是 key 名.value_or()的意思是如果有值就返回值没有就返回为什么要用 value_orget()返回的是std::optional可能有值也可能没值直接比较 optional 不方便用value_or()可以得到一个确定的字符串来比较1.6 完成全部验证现在我们再加上 key2 的验证voidtest_basic_parsing(){TEST_CASE(Basic Parsing);constchar*iniRini( key1 value1 key2 value2 )ini;IniParser parser;boolokparser.parse(ini);ASSERT_TRUE(ok,Parse should succeed);ASSERT_EQ(parser.get(,key1).value_or(),value1,Top-level key1);ASSERT_EQ(parser.get(,key2).value_or(),value2,Top-level key2);}完成这就是一个完整的基础测试了。1.7 添加 Section 测试现在我们让测试数据更复杂一点加入一个 sectionvoidtest_basic_parsing(){TEST_CASE(Basic Parsing);constchar*iniRini( key1 value1 key2 value2 [section1] key3 value3 )ini;IniParser parser;boolokparser.parse(ini);ASSERT_TRUE(ok,Parse should succeed);ASSERT_EQ(parser.get(,key1).value_or(),value1,Top-level key1);ASSERT_EQ(parser.get(,key2).value_or(),value2,Top-level key2);ASSERT_EQ(parser.get(section1,key3).value_or(),value3,Section1 key3);}注意变化INI 数据中加了[section1]和key3 value3验证时第一个参数变成了section1这就是 section 的用法恭喜第一个测试完成了✅第二步测试注释 - test_comments现在我们来测试 INI 文件的注释功能。注释是很重要的因为用户可能在配置文件里加注释注释不应该影响解析结果2.1 先写框架voidtest_comments(){TEST_CASE(Comment Handling);}2.2 准备带注释的数据INI 文件支持两种注释;和#voidtest_comments(){TEST_CASE(Comment Handling);constchar*iniRini( ; semicolon comment # hash comment key1 value1 )ini;}解释第一行是分号注释整行第二行是井号注释整行第三行是实际的键值对2.3 测试整行注释voidtest_comments(){TEST_CASE(Comment Handling);constchar*iniRini( ; semicolon comment # hash comment key1 value1 )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,key1).value_or(),value1,Value after comments);}这个测试验证前面的注释不影响 key1 的解析2.4 添加行内注释测试注释也可以出现在行尾voidtest_comments(){TEST_CASE(Comment Handling);constchar*iniRini( ; semicolon comment # hash comment key1 value1 ; inline comment key2 value2 # inline hash comment )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,key1).value_or(),value1,Value before inline semicolon comment);ASSERT_EQ(parser.get(,key2).value_or(),value2,Value before inline hash comment);}重点value1后面的; inline comment应该被忽略解析出来的值应该是value1不包含注释部分2.5 测试引号保护注释符号这是个容易出错的地方如果值本身包含;或#需要用引号保护voidtest_comments(){TEST_CASE(Comment Handling);constchar*iniRini( ; semicolon comment # hash comment key1 value1 ; inline comment key2 value2 # inline hash comment key3 value;with;semicolons ; this is a comment key4 value#with#hashes # comment )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,key1).value_or(),value1,Value before inline semicolon comment);ASSERT_EQ(parser.get(,key2).value_or(),value2,Value before inline hash comment);ASSERT_EQ(parser.get(,key3).value_or(),value;with;semicolons,Quoted semicolons preserved);ASSERT_EQ(parser.get(,key4).value_or(),value#with#hashes,Quoted hashes preserved);}关键点value;with;semicolons中的分号是值的一部分不是注释引号后面的; this is a comment才是注释解析器应该正确区分这两种情况第二个测试完成✅第三步测试 Section - test_sectionsSection 是 INI 文件的重要功能让配置可以分组。3.1 基本框架voidtest_sections(){TEST_CASE(Section Handling);}3.2 准备测试数据我们要测试几种情况顶层键在任何 section 之前不同的 section同一个 section 出现两次voidtest_sections(){TEST_CASE(Section Handling);constchar*iniRini( toplevel top [section1] key1 val1 [section2] key2 val2 [section1] key3 val3 )ini;}数据说明toplevel top在任何 section 之前[section1]出现了两次中间还有一个[section2]3.3 验证顶层键voidtest_sections(){TEST_CASE(Section Handling);constchar*iniRini( toplevel top [section1] key1 val1 [section2] key2 val2 [section1] key3 val3 )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,toplevel).value_or(),top,Top-level key);}验证section 为空字符串的键能正确读取3.4 验证不同的 sectionvoidtest_sections(){TEST_CASE(Section Handling);constchar*iniRini( toplevel top [section1] key1 val1 [section2] key2 val2 [section1] key3 val3 )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,toplevel).value_or(),top,Top-level key);ASSERT_EQ(parser.get(section1,key1).value_or(),val1,Section1 first key);ASSERT_EQ(parser.get(section2,key2).value_or(),val2,Section2 key);}3.5 验证同一 section 多次出现voidtest_sections(){TEST_CASE(Section Handling);constchar*iniRini( toplevel top [section1] key1 val1 [section2] key2 val2 [section1] key3 val3 )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,toplevel).value_or(),top,Top-level key);ASSERT_EQ(parser.get(section1,key1).value_or(),val1,Section1 first key);ASSERT_EQ(parser.get(section1,key3).value_or(),val3,Section1 second occurrence);ASSERT_EQ(parser.get(section2,key2).value_or(),val2,Section2 key);}重要[section1]出现两次两次的键应该都能访问到第三个测试完成✅第四步测试引号值 - test_quoted_values引号让我们可以在值中包含特殊字符。4.1 框架和基本引号voidtest_quoted_values(){TEST_CASE(Quoted Values);constchar*iniRini( single quoted value )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,single).value_or(),quoted value,Single quoted);}测试基本的引号值4.2 添加空格测试引号的一个重要作用是保留空格voidtest_quoted_values(){TEST_CASE(Quoted Values);constchar*iniRini( single quoted value double value with spaces )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,single).value_or(),quoted value,Single quoted);ASSERT_EQ(parser.get(,double).value_or(),value with spaces,Spaces in quotes);}4.3 测试空引号voidtest_quoted_values(){TEST_CASE(Quoted Values);constchar*iniRini( single quoted value double value with spaces empty )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,single).value_or(),quoted value,Single quoted);ASSERT_EQ(parser.get(,double).value_or(),value with spaces,Spaces in quotes);ASSERT_EQ(parser.get(,empty).value_or(),,Empty quotes);}测试应该表示空字符串4.4 引号中的等号等号在 INI 中是特殊字符但引号可以保护它voidtest_quoted_values(){TEST_CASE(Quoted Values);constchar*iniRini( single quoted value double value with spaces empty withequals keyvalue )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,single).value_or(),quoted value,Single quoted);ASSERT_EQ(parser.get(,double).value_or(),value with spaces,Spaces in quotes);ASSERT_EQ(parser.get(,empty).value_or(),,Empty quotes);ASSERT_EQ(parser.get(,withequals).value_or(),keyvalue,Equals in quotes);}4.5 完整版本加入密码示例voidtest_quoted_values(){TEST_CASE(Quoted Values);constchar*iniRini( single quoted value double value with spaces empty withequals keyvalue password pss;word )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,single).value_or(),quoted value,Single quoted);ASSERT_EQ(parser.get(,double).value_or(),value with spaces,Spaces in quotes);ASSERT_EQ(parser.get(,empty).value_or(),,Empty quotes);ASSERT_EQ(parser.get(,withequals).value_or(),keyvalue,Equals in quotes);ASSERT_EQ(parser.get(,password).value_or(),pss;word,Special chars in quotes);}测试密码中的;被引号保护不会被当作注释第四个测试完成✅第五步测试转义序列 - test_escape_sequences转义序列让我们可以表示特殊字符。5.1 换行符\nvoidtest_escape_sequences(){TEST_CASE(Escape Sequences);constchar*iniRini( newline line1\nline2 )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,newline).value_or(),line1\nline2,Newline escape);}重点INI 文件中是\n两个字符解析后应该变成真正的换行符一个字符5.2 添加制表符\tvoidtest_escape_sequences(){TEST_CASE(Escape Sequences);constchar*iniRini( newline line1\nline2 tab col1\tcol2 )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,newline).value_or(),line1\nline2,Newline escape);ASSERT_EQ(parser.get(,tab).value_or(),col1\tcol2,Tab escape);}5.3 反斜杠和引号voidtest_escape_sequences(){TEST_CASE(Escape Sequences);constchar*iniRini( newline line1\nline2 tab col1\tcol2 backslash path\\to\\file quote say \hello\ )ini;IniParser parser;parser.parse(ini);ASSERT_EQ(parser.get(,newline).value_or(),line1\nline2,Newline escape);ASSERT_EQ(parser.get(,tab).value_or(),col1\tcol2,Tab escape);ASSERT_EQ(parser.get(,backslash).value_or(),path\\to\\file,Backslash escape);ASSERT_EQ(parser.get(,quote).value_or(),say \hello\,Quote escape);}转义规则\\→\一个反斜杠\→一个引号第五个测试完成✅继续下一组…现在你已经掌握了基本模式接下来的测试都遵循相同的步骤写TEST_CASE标题准备测试数据Rini( ... )ini创建解析器并解析用ASSERT_EQ、ASSERT_TRUE等验证结果让我快速过一下剩余的测试思路…第六步空白字符处理 - test_whitespace_handling测试目标解析器应该智能处理空格voidtest_whitespace_handling(){TEST_CASE(Whitespace Handling);constchar*iniRini( key1value1 // 没有空格 key2 value2 // 正常空格 key3 value3 // 多个空格 key4 value4 // 前后都有空格 )ini;// 解析后所有值都应该是干净的没有多余空格}期望不管有多少空格值都应该被正确提取第七步空值 - test_empty_values测试目标值可以是空的voidtest_empty_values(){TEST_CASE(Empty Values);constchar*iniRini( empty1 empty2 empty3 quoted_empty )ini;// 所有这些都应该返回空字符串}注意使用.value_or(NONE)来测试空值和不存在的区别第八步边界情况 - test_edge_cases测试目标处理各种极端情况voidtest_edge_cases(){TEST_CASE(Edge Cases);// 情况1完全空的文件{IniParser parser;boolokparser.parse();ASSERT_TRUE(ok,Empty file should parse);}// 情况2只有注释{constchar*ini; comment\n# comment;IniParser parser;boolokparser.parse(ini);ASSERT_TRUE(ok,Comments-only should parse);}// ... 更多边界情况}用花括号{ }的原因创建独立的作用域避免变量名冲突第九步复杂真实案例 - test_complex_real_world测试目标模拟真实配置文件voidtest_complex_real_world(){TEST_CASE(Complex Real-World Example);constchar*iniRini( ; Application Configuration app_name MyApp version 1.2.3 [server] host 0.0.0.0 port 8080 [database] driver postgresql password Pssw0rd!123 ; secure password )ini;// 验证多个 section多种类型的值}价值确保在复杂场景下也能正确工作Summary每个测试都遵循这个模式voidtest_xxx(){// 1. 标题TEST_CASE(描述);// 2. 准备数据constchar*iniRini( ... 测试数据 ... )ini;// 3. 解析IniParser parser;parser.parse(ini);// 4. 验证ASSERT_EQ(实际值,期望值,描述);ASSERT_TRUE(条件,描述);ASSERT_FALSE(条件,描述);}关键技巧用原始字符串Rini( ... )ini写测试数据用.value_or()处理 optional每个断言都写清楚描述从简单到复杂逐步添加测试完整的 main 函数把所有测试组织起来intmain(){std::coutstd::endl;std::cout INI Parser Comprehensive Test Suitestd::endl;std::coutstd::endl;test_basic_parsing();test_comments();test_sections();test_quoted_values();test_escape_sequences();test_whitespace_handling();test_empty_values();test_multiline_and_continuation();test_edge_cases();test_invalid_syntax();test_special_characters();test_section_ordering();test_complex_real_world();test_data_iteration();std::cout\nstd::endl;std::coutTest Results:std::endl;std::cout Passed: tests_passedstd::endl;std::cout Failed: tests_failedstd::endl;std::cout Total: (tests_passedtests_failed)std::endl;std::coutstd::endl;if(tests_failed0){std::cout\n All tests passed!std::endl;return0;}else{std::cout\n❌ Some tests failed!std::endl;return1;}}