C++ - 仿 RabbitMQ 实现消息队列--sqlite与gtest快速上手

发布于:2025-07-17 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

SQLite

什么是 SQLite

为什么要用 SQLite

SQLite3 C/C++ API 介绍

SQLite3 C/C++ API 使用

GTest

GTest 是什么

GTest 使用

TEST 宏

断言

事件机制

全局事件

TestSuite 事件


SQLite

什么是 SQLite

        SQLite 是一个进程内的轻量级数据库,它实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它是一个零配置的数据库,这意味着与其他数据库不一样,我们不需要在系统中配置。像其他数据库,SQLite 引擎不是一个独立的进程,可以按应用程序需求进行静态或动态连接,SQLite 直接访问其存储文件。

为什么要用 SQLite

  • 不需要一个单独的服务器进程或操作的系统(无服务器的)。
  • SQLite 不需要配置。
  • 一个完整的 SQLite 数据库是存储在一个单一的跨平台的磁盘文件。
  • SQLite 是非常小的,是轻量级的,完全配置时小于 400KiB,省略可选功能配置时小于250KiB。
  • SQLite 是自给自足的,这意味着不需要任何外部的依赖。
  • SQLite 事务是完全兼容 ACID 的,允许从多个进程或线程安全访问。
  • SQLite 支持 SQL92(SQL2)标准的大多数查询语言的功能。
  • SQLite 使用 ANSI-C 编写的,并提供了简单和易于使用的 API。
  • SQLite 可在 UNIX(Linux, Mac OS-X, Android, iOS)和 Windows(Win32, WinCE, WinRT)中运行。

SQLite3 C/C++ API 介绍

        C/C++ API 是 SQLite3 数据库的一个客户端, 提供一种用 C/C++操作数据库的方法。下面我们介绍一下常见的几个接口:

sqlite3 操作流程:
0. 查看当前数据库在编译阶段是否启动了线程安全
         int sqlite3_threadsafe(); 0-未启用; 1-启用
         需要注意的是 sqlite3 是有三种安全等级的:

  1. 非线程安全模式
  2. 线程安全模式(不同的连接在不同的线程/进程间是安全的,即一个句柄不能用于多线程间)
  3.  串行化模式(可以在不同的线程/进程间使用同一个句柄)

1. 创建/打开数据库文件,并返回操作句柄
         int sqlite3_open(const char *filename, sqlite3 **ppDb) 成功返回SQLITE_OK
 //若在编译阶段启动了线程安全,则在程序运行阶段可以通过参数选择线程安全等级
         int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs );

flag: 

  1.  SQLITE_OPEN_READWRITE -- 以可读可写方式打开数据库文件
  2.  SQLITE_OPEN_CREATE -- 不存在数据库文件则创建
  3.  SQLITE_OPEN_NOMUTEX--多线程模式,只要不同的线程使用不同的连接即可保证线程安全
  4.  SQLITE_OPEN_FULLMUTEX--串行化模式

 返回:SQLITE_OK 表示成功
2. 执行语句
         int sqlite3_exec(sqlite3*, char *sql, int (*callback)(void*,int,char**,char**),  void* arg, char **err)   返回:SQLITE_OK 表示成功

  1. sqlite3*:数据库文件句柄。
  2. char *sql:sql语句。
  3. int (*callback)(void*,int,char**,char**):对执行结果的回调函数。
  4. void* arg:给回调函数的第一个参数,常用于保存处理结果。
  5. char **err:错误信息。

        int (*callback)(void*,int,char**,char**)

  1.  void* : 是设置的在回调时传入的 arg 参数。
  2.  int:一行中数据的列数。
  3.  char**:存储一行数据的字符指针数组。
  4.  char**:每一列的字段名称。

         对于每一行结果都会执行这个回调函数,这个回调函数有个 int 返回值,成功处理的情况下必须返回 0,返回非 0会触发 ABORT 退出程序
3. 销毁句柄
         int sqlite3_close(sqlite3* db); 成功返回 SQLITE_OK
         int sqlite3_close_v2(sqlite3*); 推荐使用--无论如何都会返回SQLITE_OK
        获取错误信息
         const char *sqlite3_errmsg(sqlite3* db);

SQLite3 C/C++ API 使用

        下面我们将这几个接口封装成一个类,快速上手这几个接口

// 封装一个sqlitehelper类
#pragma once

#include <sqlite3.h>
#include <iostream>
#include <string>

class SqliteHelper
{
public:
    typedef int (*sqliteCallback)(void*, int, char**, char**);
    SqliteHelper(const std::string &dbfile)
        :_dbfile(dbfile), _handler(nullptr) {}

    bool open(int safe_level = SQLITE_OPEN_FULLMUTEX)
    {
        int ret = sqlite3_open_v2(_dbfile.c_str(), &_handler, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | safe_level, nullptr);
        if (ret != SQLITE_OK)
        {
            std::cerr << "打开/创建数据库失败: " << sqlite3_errmsg(_handler) << std::endl;
            return false;
        }
        return true;
    }

    bool exec(const std::string &sql, sqliteCallback callback, void *arg)
    {
        int ret = sqlite3_exec(_handler, sql.c_str(), callback, arg, nullptr);
        if (ret != SQLITE_OK)
        {
            std::cerr << sql << std::endl;
            std::cerr << "执行语句失败: " << sqlite3_errmsg(_handler) << std::endl;
            return false;
        }
        return true;
    }

    void close()
    {
        if(_handler)
            sqlite3_close_v2(_handler);
    }
private:
    std::string _dbfile;
    sqlite3 *_handler;
};

测试程序:

#include "sqlite.hpp"
#include <cassert>
#include <vector>

int select_stu_callback(void *arg, int col_count, char **result, char **fields_name)
{
    std::vector<std::string> *names = (std::vector<std::string>*)arg;
    names->push_back(result[0]);
    return 0;
}

int main()
{
    SqliteHelper helper("./test.db");
    // 创建/打开库文件
    assert(helper.open());
    // 创建表
    const char *ct = "create table if not exists student(sn int primary key, name varchar(32), age int);";
    assert(helper.exec(ct, nullptr, nullptr));
    // 新增数据
    const char *insert_sql = "insert into student values(1, '小明', 18), (2, '小刚', 19), (3, '小红', 18);";
    assert(helper.exec(insert_sql, nullptr, nullptr));

    const char *select_sql = "select name from student;";
    std::vector<std::string> names;
    assert(helper.exec(select_sql, select_stu_callback, &names));
    for (auto &name : names)
    {
        std::cout << name << std::endl;
    }
    // 关闭数据库
    helper.close();
    return 0;
}

GTest

GTest 是什么

        GTest 是一个跨平台的 C++单元测试框架,由 google 公司发布。gtest 是为了在不同平台上为编写 C++单元测试而生成的。它提供了丰富的断言、致命和非致命判断、参数化等等。

GTest 使用

TEST 宏

TEST(test_case_name, test_name)
TEST_F(test_fixture,test_name)
  • TEST:主要用来创建一个简单测试, 它定义了一个测试函数, 在这个函数中可以使用任何 C++代码并且使用框架提供的断言进行检查。
  • TEST_F:主要用来进行多样测试,适用于多个测试场景如果需要相同的数据配置的情况, 即相同的数据测不同的行为。

断言

GTest 中的断言的宏可以分为两类:

  1. ASSERT_系列:如果当前点检测失败则退出当前函数。
  2. EXPECT_系列:如果当前点检测失败则继续往下执行。

下面是经常使用的断言介绍:

// bool 值检查
ASSERT_TRUE(参数),期待结果是 true
ASSERT_FALSE(参数),期待结果是 false
//数值型数据检查
ASSERT_EQ(参数 1,参数 2),传入的是需要比较的两个数 equal
ASSERT_NE(参数 1,参数 2),not equal,不等于才返回 true
ASSERT_LT(参数 1,参数 2),less than,小于才返回 true
ASSERT_GT(参数 1,参数 2),greater than,大于才返回 true
ASSERT_LE(参数 1,参数 2),less equal,小于等于才返回 true
ASSERT_GE(参数 1,参数 2),greater equal,大于等于才返回 true

下面我们做一个测试:

#include <iostream>
#include <gtest/gtest.h>

// 断言宏的使用 ASSERT_ 断言失败则退出   EXPECT_断言失败继续运行   必须在单元测试宏函数中使用

TEST(test, great_than)
{
    int age = 20;
    ASSERT_GT(age, 18);
    printf("OK\n");
}

int main(int argc, char *argv[])
{
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();    
}

运行结果:

[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from test
[ RUN      ] test.great_than
OK
[       OK ] test.great_than (0 ms)
[----------] 1 test from test (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.

事件机制

        GTest 中的事件机制是指在测试前和测试后提供给用户自行添加操作的机制,而且该机制也可以让同一测试套件下的测试用例共享数据。GTest 框架中事件的结构层次:

  1.  测试程序:一个测试程序只有一个 main 函数,也可以说是一个可执行程序是一个测试程序。该级别的事件机制是在程序的开始和结束执行。
  2. 测试套件:代表一个测试用例的集合体,该级别的事件机制是在整体的测试案例开始和结束执行。
  3. 测试用例:该级别的事件机制是在每个测试用例开始和结束都执行。

        事件机制的最大好处就是能够为我们各个测试用例提前准备好测试环境,并在测试完毕后用于销毁环境,这样有个好处就是如果我们有一端代码需要进行多种不同方法的测试,则可以通过测试机制在每个测试用例进行之前初始化测试环境和数据,并在测试完毕后清理测试造成的影响。

 GTest 提供了三种常见的的事件:

全局事件

        针对整个测试程序。实现全局的事件机制,需要创建一个自己的类,然后继承 testing::Environment 类,然后分别实现成员函数 SetUp 和 TearDown,同时在 main 函数内进行调用 testing::AddGlobalTestEnvironment(new MyEnvironment);函数添加全局的事件机制。

#include <iostream>
#include <gtest/gtest.h>
#include <unordered_map>
#include <string>

class MyEnvironment : public testing::Environment
{
public:
    virtual void SetUp() override
    {
        std::cout << "单元测试环境初始化" << std::endl;
    }

    virtual void TearDown() override
    {
        std::cout << "单元测试环境清理" << std::endl;
    }
};

TEST(MyEnvironment, test1)
{
    std::cout << "单元测试1" << std::endl;
}

TEST(MyEnvironment, test2)
{
    std::cout << "单元测试2" << std::endl;
}



std::unordered_map<std::string, std::string> mymap;
class MyMapTest : public testing::Environment
{
public:
    virtual void SetUp() override
    {
        std::cout << "单元测试环境初始化" << std::endl;
        mymap.insert(std::make_pair("hello", "你好"));
        mymap.insert(std::make_pair("bye", "再见"));
    }

    virtual void TearDown() override
    {
        std::cout << "单元测试环境清理" << std::endl;
        mymap.clear();
    }
};

TEST(MyMapTest, test1)
{
    ASSERT_EQ(mymap.size(), 2);
    mymap.erase("hello");
}

TEST(MyMapTest, test2)
{
    ASSERT_EQ(mymap.size(), 2);
}

int main(int argc, char *argv[])
{
    testing::AddGlobalTestEnvironment(new MyEnvironment);
    testing::AddGlobalTestEnvironment(new MyMapTest);
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

运行结果:

[==========] Running 4 tests from 2 test suites.
[----------] Global test environment set-up.
单元测试环境初始化
单元测试环境初始化
[----------] 2 tests from MyEnvironment
[ RUN      ] MyEnvironment.test1
单元测试1
[       OK ] MyEnvironment.test1 (0 ms)
[ RUN      ] MyEnvironment.test2
单元测试2
[       OK ] MyEnvironment.test2 (0 ms)
[----------] 2 tests from MyEnvironment (0 ms total)

[----------] 2 tests from MyMapTest
[ RUN      ] MyMapTest.test1
[       OK ] MyMapTest.test1 (0 ms)
[ RUN      ] MyMapTest.test2
global.cc:58: Failure
Expected equality of these values:
  mymap.size()
    Which is: 1
  2
[  FAILED  ] MyMapTest.test2 (0 ms)
[----------] 2 tests from MyMapTest (0 ms total)

[----------] Global test environment tear-down
单元测试环境清理
单元测试环境清理
[==========] 4 tests from 2 test suites ran. (0 ms total)
[  PASSED  ] 3 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] MyMapTest.test2

 1 FAILED TEST
TestSuite 事件

        针对一个个测试套件。测试套件的事件机制我们同样需要去创建一个类,继承自testing::Test,实现两个静态函数 SetUpTestCase 和TearDownTestCase,测试套件的事件机制不需要像全局事件机制一样在 main 注册,而是需要将我们平时使用的 TEST 宏改为 TEST_F 宏。

  •  SetUpTestCase() 函数是在测试套件第一个测试用例开始前执行。
  • TearDownTestCase() 函数是在测试套件最后一个测试用例结束后执行。
  • 需要注意 TEST_F 的第一个参数是我们创建的类名,也就是当前测试套件的名称,这样在 TEST_F 宏的测试套件中就可以访问类中的成员了。
#include <iostream>
#include <gtest/gtest.h>
#include <unordered_map>
#include <string>

class MyTest : public testing::Test
{
public:
    static void SetUpTestCase()
    {
        std::cout << "所有单元测试前初始化环境\n";
    }

    static void TearDownTestCase()
    {
        std::cout << "所有单元测试完毕后清理环境\n";
    }

    std::unordered_map<std::string, std::string> _mymap;
};

TEST_F(MyTest, insert_test)
{
    _mymap.insert(std::make_pair("good", "好"));
}

TEST_F(MyTest, size_test)
{
    ASSERT_EQ(_mymap.size(), 1);
}

int main(int argc, char *argv[])
{
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

 运行结果:

[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from MyTest
所有单元测试前初始化环境
[ RUN      ] MyTest.insert_test
[       OK ] MyTest.insert_test (0 ms)
[ RUN      ] MyTest.size_test
suit.cc:40: Failure
Expected equality of these values:
  _mymap.size()
    Which is: 0
  1
[  FAILED  ] MyTest.size_test (0 ms)
所有单元测试完毕后清理环境
[----------] 2 tests from MyTest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] MyTest.size_test

 1 FAILED TEST

        能够看到在上例中,有一个好处,就是将数据与测试结合到同一个测试环境类中了,这样与外界的耦合度更低,代码也更清晰。
        但是同样的,我们发现在两个测试用例中第二个测试用例失败了,这是为什么呢?这就涉及到了 TestCase 事件的机制。
        • TestCase 事件: 针对一个个测试用例。测试用例的事件机制的创建和测试套件的基本一样,不同地方在于测试用例实现的两个函数分别是 SetUp 和 TearDown, 这两个函数也不是静态函数

        ○ SetUp()函数是在一个测试用例的开始前执行

        ○ TearDown()函数是在一个测试用例的结束后执行
        也就是说,在 TestSuite/TestCase 事件中,每个测试用例,虽然它们同用同一个事件环境类,可以访问其中的资源,但是本质上每个测试用例的环境都是独立的,这样我们就不用担心不同的测试用例之间会有数据上的影响了,保证所有的测试用例都使用相同的测试环境进行测试。

 

#include <iostream>
#include <gtest/gtest.h>
#include <unordered_map>
#include <string>

class MyTest : public testing::Test
{
public:
    static void SetUpTestCase()
    {
        std::cout << "所有单元测试前初始化环境\n";
    }

    static void TearDownTestCase()
    {
        std::cout << "所有单元测试完毕后清理环境\n";
    }

    void SetUp() override
    {
        _mymap.insert(std::make_pair("hello", "你好"));
        _mymap.insert(std::make_pair("bye", "再见"));
    }

    void TearDown() override
    {
        _mymap.clear();
    }

    std::unordered_map<std::string, std::string> _mymap;
};

TEST_F(MyTest, insert_test)
{
    _mymap.insert(std::make_pair("good", "好"));
}

TEST_F(MyTest, size_test)
{
    ASSERT_EQ(_mymap.size(), 2);
}

int main(int argc, char *argv[])
{
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

运行结果:

[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from MyTest
所有单元测试前初始化环境
[ RUN      ] MyTest.insert_test
[       OK ] MyTest.insert_test (0 ms)
[ RUN      ] MyTest.size_test
[       OK ] MyTest.size_test (0 ms)
所有单元测试完毕后清理环境
[----------] 2 tests from MyTest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 2 tests.


网站公告

今日签到

点亮在社区的每一天
去签到