ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【C++ 标准项目】发布订阅消息队列(篇四):sqlite 与 gtest 断言框架介绍及实战应用

【C++ 标准项目】发布订阅消息队列(篇四):sqlite 与 gtest 断言框架介绍及实战应用 前面我们介绍了muduo库接下来我们进行sqlite以及gtest的使用一.SQLite31.什么是 SQLiteSQLite 是一个进程内的轻量级数据库它实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它是一个零配置的数据库这意味着与其他数据库不一样我们不需要在系统中配置。不像其他数据库SQLite 引擎不是一个独立的进程可以按应用程序需求进行静态或动态连接SQLite 直接访问其存储文件。2.为什么要用 SQLite不需要一个单独的服务器进程或操作的系统无服务器SQLite 不需要配置一个完整的 SQLite 数据库是存储在一个单一的跨平台磁盘文件里SQLite 非常小、轻量级完全配置时小于 400 KiB省略可选功能配置时小于 250 KiBSQLite 是自给自足的这意味着不需要任何外部依赖SQLite 事务完全兼容 ACID允许从多个进程或线程安全访问SQLite 支持 SQL92SQL2标准的大多数查询语言功能SQLite 使用 ANSI-C 编写并提供了简单、易于使用的 APISQLite 可在 UNIXLinux、Mac OS-X、Android、iOS和 WindowsWin32、WinCE、WinRT中运行。3.SQLite3 C/C API 介绍C/C API 是 SQLite3 数据库的一个客户端提供一种用 C/C 操作数据库的方法。官方文档List Of SQLite Functions1.查看当前数据库在编译阶段是否启动了线程安全int sqlite3_threadsafe(); // 0-未启用1-启用需要注意的是SQLite 有三种安全等级非线程安全模式线程安全模式不同的连接在不同的线程/进程间是安全的即一个句柄不能用于多线程间串行化模式可以在不同的线程/进程间使用同一个句柄。2. 创建/打开数据库文件并返回操作句柄int sqlite3_open(const char *filename, sqlite3 **ppDb); // 成功返回 SQLITE_OK若在编译阶段启动了线程安全则在程序运行阶段可以通过参数选择线程安全等级int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs); // 返回 SQLITE_OK 表示成功flags取值取值含义SQLITE_OPEN_READWRITE以可读可写方式打开数据库文件SQLITE_OPEN_CREATE不存在数据库文件则创建SQLITE_OPEN_NOMUTEX多线程模式只要不同的线程使用不同的连接即可保证线程安全SQLITE_OPEN_FULLMUTEX串行化模式3. 执行语句int sqlite3_exec(sqlite3 *db, const char *sql, int (*callback)(void*, int, char**, char**), void *arg, char **errmsg); // 返回 SQLITE_OK 表示成功回调函数int (*callback)(void*, int, char**, char**)的四个参数参数含义void*在回调时传入的arg参数int一行中数据的列数char**存储一行数据的字符指针数组char**每一列的字段名称这个回调函数有个int返回值成功处理的情况下必须返回 0返回非 0 会触发 ABORT 退出程序。4. 销毁句柄int sqlite3_close(sqlite3 *db); // 成功返回 SQLITE_OK int sqlite3_close_v2(sqlite3 *db); // 推荐使用——无论如何都会返回 SQLITE_OK获取错误信息const char *sqlite3_errmsg(sqlite3 *db);4.SQLite3 C/C API的使用来看代码我们直接进行封装方便我们的使用代码框架SqliteHelper ├── 成员 │ ├── _dbfile : std::string 数据库文件路径 │ └── _handler : sqlite3* 数据库操作句柄核心资源 │ ├── open() → sqlite3_open_v2() 创建/打开拿到句柄 ├── exec() → sqlite3_exec() 执行任意 SQL └── close() → sqlite3_close_v2() 释放句柄程序运行检测我们先开放插入代码运行结果接着开放删除语句进行执行接着往下运行会发现报错了原因如下所以接下来进行修改接着再次运行看结果第二次运行就主键冲突原因test.db是持久化文件第一次运行插入的数据还在磁盘上。而sn是主键create table if not exists student(sn int primary key, name varchar(32), age int);同样的主键1、2、3再插一次必然冲突。这恰恰是 SQLite 的特性它的价值就在于数据不随程序退出而消失——一个文件就是一个完整数据库。所以调试时需要主动清库。三种解法方案做法适用删库重来rm -f test.db ./main调试阶段最省事有则替换SQL 改成insert or replace into student values(...)幂等写入先清空表插入前执行delete from student;数据量小注意哈SQLite 里int primary key不会自增必须写integer primary key是integer而不是int才会成为 rowid 的别名并自动递增。用int的话主键值要自己保证不重复。assert在这里暴露了它的问题assert检查失败时做了什么直接 abort 进程 core dump。而更麻烦的是它在 Release 构建下的行为构建方式断言失败时Debug默认当前abort core dump用户看到的是崩溃Release-DNDEBUG断言被整个编译掉→ 插入失败了程序照常往下跑后续查询拿到的是旧数据你完全不知道出过错两种情况都不是想要的结果。因为assert的正确定位是检查不可能发生的内部逻辑错误比如这个指针按设计绝不可能是空。而打开数据库执行 SQL都是可能失败的外部操作——文件可能不存在、磁盘可能满、SQL 可能写错。这类检查应该用ifif (!helper.exec(insert_sql, nullptr, nullptr)) { std::cerr 插入失败 std::endl; return -1; // 优雅退出不 core dump }简单来说就是能被编译掉的检查就不是检查。验证结果只打印两行是正确的——因为同时delete from student where sn3也放开了插入 3 行张小明(1) / 小黑(2) / 小红(3) ---delete sn3(小红被删除) ---select(只剩张小明、小黑)二.Gtest1.GTest 是什么GTest 是一个跨平台的 C单元测试框架由 google 公司发布。gtest 是为了在不同平台上为编写 C单元测试而生成的。它提供了丰富的断言、致命和非致命判断、参数化等等2.GTest 使用a.两个核心宏TEST(test_case_name, test_name) TEST_F(test_fixture, test_name)宏用途TEST创建一个简单测试。函数体内可以写任意 C 代码并用框架提供的断言检查TEST_F用于多样测试。多个测试场景需要相同的数据配置时使用——即相同的数据测不同的行为简单区分TEST每次都是干净的空白开始TEST_F基于一个Fixture 类可以在每个测试用例执行前自动准备同一份数据。b.断言两类差别很大系列失败时的行为ASSERT_*退出当前函数后面的代码不再执行EXPECT_*继续往下执行失败也把整个函数跑完c.常用断言速查// bool 值检查 ASSERT_TRUE(参数); // 期待结果是 true ASSERT_FALSE(参数); // 期待结果是 false // 数值型数据检查 ASSERT_EQ(val1, val2); // equal相等才通过 ASSERT_NE(val1, val2); // not equal不等才通过 ASSERT_LT(val1, val2); // less than小于才通过 ASSERT_GT(val1, val2); // greater than大于才通过 ASSERT_LE(val1, val2); // less equal小于等于才通过 ASSERT_GE(val1, val2); // greater equal大于等于才通过把上面所有ASSERT_换成EXPECT_就是对应的非致命版本。自定义失败信息如果对自动输出的错误信息不满意可以用operator在失败时打印自己的日志ASSERT_TRUE(abs(1) 1) abs(1)1;实战测试一个求绝对值的函数运行结果如下参数逐个说明参数作用能不能省-stdc11GTest 需要 C11 起不能main.cc源文件—-o main输出可执行文件名可以默认a.out-lgtest链接 GTest 库不能-pthreadGTest 内部用到线程不能接下来我们呢将第一条语句改成 ASSERT_FALSE 进行错误测试【重点】为什么只报了一条错上面的失败输出里只出现了第 11 行那一条失败后面 8 条断言一条信息都没有。不是它们都通过了而是根本没执行。TEST(abs_test, test1) { ASSERT_FALSE(abs(1) 1); // -- 这里失败函数【直接返回】 ASSERT_TRUE(abs(-1) 1); // -- 以下 8 条全部被跳过从未运行 ASSERT_FALSE(abs(-2) -2); ... }这就是ASSERT_和EXPECT_的本质区别用哪个结果ASSERT_FALSE第一条失败 --函数返回-- 只看到 1 条失败信息EXPECT_FALSE第一条失败 --继续跑完-- 看到 1 条失败 8 条通过d.事件机制GTest 中的事件机制是指在测试前和测试后提供给用户自行添加操作的机制而且该机制也可以让同一测试套件下的测试用例共享数据。GTest 框架中事件的结构层次测试程序一个测试程序只有一个 main 函数也可以说是一个可执行程序是一个测试程序。该级别的事件机制是在程序的开始和结束执行测试套件代表一个测试用例的集合体该级别的事件机制是在整体的测试案例开始和结束执行测试用例该级别的事件机制是在每个测试用例开始和结束都执行事件机制的最大好处就是能够为我们各个测试用例提前准备好测试环境并在测试完毕后用于销毁环境这样有个好处就是如果我们有一端代码需要进行多种不同方法的测试则可以通过测试机制在每个测试用例进行之前初始化测试环境和数据并在测试完毕后清理测试造成的影响。GTest 提供了三种常见的的事件全局事件针对整个测试程序。实现全局的事件机制需要创建一个自己的类然后继承 testing::Environment 类然后分别实现成员函数 SetUp 和 TearDown同时在 main 函数内进行调用 testing::AddGlobalTestEnvironment(newMyEnvironment);函数添加全局的事件机制TestSuite 事件针对一个个测试套件。测试套件的事件机制我们同样需要去创建一个类继承自 testing::Test实现两个静态函数 SetUpTestCase 和TearDownTestCase测试套件的事件机制不需要像全局事件机制一样在 main 注册而是需要将我们平时使用的 TEST 宏改为 TEST_F 宏。 SetUpTestCase() 函数是在测试套件第一个测试用例开始前执行 TearDownTestCase() 函数是在测试套件最后一个测试用例结束后执行 需要注意 TEST_F 的第一个参数是我们创建的类名也就是当前测试套件的名称这样在 TEST_F 宏的测试套件中就可以访问类中的成员了。能够看到在上例中有一个好处就是将数据与测试结合到同一个测试环境类中了这样与外界的耦合度更低代码也更清晰。但是同样的我们发现在两个测试用例中第二个测试用例失败了这是为什么呢这就涉及到了 TestCase 事件的机制。TestCase 事件:针对一个个测试用例。测试用例的事件机制的创建和测试套件的基本一样不同地方在于测试用例实现的两个函数分别是 SetUp 和 TearDown, 这两个函数也不是静态函数 SetUp()函数是在一个测试用例的开始前执行 TearDown()函数是在一个测试用例的结束后执行也就是说在 TestSuite/TestCase 事件中每个测试用例虽然它们同用同一个事件环境类可以访问其中的资源但是本质上每个测试用例的环境都是独立的这样我们就不用担心不同的测试用例之间会有数据上的影响了保证所有的测试用例都使用相同的测试环境进行测试。3.GTest 三层事件1.先搞懂三层是什么GTest 的事件机制就是在测试前自动准备、测试后自动收拾。它分三层区别只有一件事这个准备和收拾多久做一次用上课打个比方层次比方做几次全局事件学校大门早上开、晚上关全校1 次TestSuite 事件一个班上课前老师进教室开灯、擦黑板一个班1 次Test 事件每个学生各拿一张全新的草稿纸每个学生1 次一句话就是全局和 TestSuite 是一群人共用一次Test 是每个人各来一次。2.三个例子分别在干什么例子一例子二例子三做什么全校开一次门一个班擦一次黑板擦黑板 每人发新纸继承谁testing::Environmenttesting::Testtesting::Test写哪些函数SetUp/TearDownSetUpTestCase/TearDownTestCase四个都写用哪个宏TESTTEST_FTEST_Fmain 里要注册吗要不用不用结果2 个全过挂了一个2 个全过3.例子二为什么挂了一个GTest 有个规矩每跑一个测试用例就发一张全新的草稿纸。跑 insert_test 这个测试 ↓ GTest 发了【草稿纸 A】 在纸 A 上写了 3 条数据Hello / hello / 雷吼 查 hello -- 查到了通过 ↓ 测试结束【草稿纸 A 被收走连同上面的数据一起扔掉】 跑 sizeof 这个测试 ↓ GTest 发了【草稿纸 B】—— 一张全新的空纸 查 dict.size() -- 是 0 ASSERT_GT(0, 0) -- 不成立 -- 失败为什么数据会跟着纸走因为dict是类的非静态成员——它就相当于写在草稿纸上的内容。定义方式相当于结果std::unordered_map... dict;非静态写在草稿纸上换张纸就没了 →不共享static std::unordered_map... dict;静态写在黑板上全班都看得到 →共享注意这个失败不是 GTest 的 bug反而是故意的设计——保证每个测试都从干净的环境开始不会因为谁先跑谁后跑而结果不同。4.例子三怎么把这个坑填上例子三只比例子二多做了一件事加了SetUp/TearDown。SetUp的作用相当于每次发新草稿纸时老师先替你把基础数据抄上去。跑 insert_test ↓ 先自动执行 SetUp往新纸上写 2 条bye / see you ↓ 测试体再自己加 1 条hello 总共 3 条 -- ASSERT_EQ(dict.size(), 3) -- 通过 ↓ 自动执行 TearDown把纸清空 ↓ 草稿纸被收走 跑 size_test ↓ 先自动执行 SetUp又往新纸上写同样的 2 条 查 hello -- 查不到因为新纸上只有 bye / see you 总共 2 条 -- ASSERT_EQ(dict.size(), 2) -- 通过结果就是每个测试拿到的初始数据都一模一样两个都过了。5.三个例子的运行次数对照看日志出现了几次就能看出作用范围——这是最直观的证据日志例子一例子二例子三全局SetUp/TearDown1 / 1——套件SetUpTestCase/TearDownTestCase—1 / 11 / 1用例SetUp/TearDown——2 / 26.例子二 -- 例子三只改了一处改了什么效果加了SetUp/TearDown每个用例前重填数据FAILED → PASSED其他全一样同样testing::Test、同样TEST_F、同样非静态dict—7.实际写代码时怎么选你要做的事用哪一层为什么连接数据库、加载全局配置全局事件整个程序只需要做一次建表、加载一批只读的基础数据TestSuite 事件一组测试共用一份每个测试都要一份干净、一致的数据Test 事件最常用避免测试之间互相干扰为什么推荐优先用 Test 事件因为它能避免最让人头疼的一类问题单个测试跑能过一起跑就挂。 这类问题排查起来极其耗时而根源往往就是不同测试共享了可变数据。总结SetUpTestCase管整组一次SetUp管每个测试各一次。 例子二的失败不是写错了而是暴露了测试之间数据不共享这个事实例子三用SetUp把它补上就对了。
返回列表