【C++第三方库】快速上手---轻量级数据库SQLite和单元测试工具Gtest

每日激励,"驾驭命运的舵是奋斗。不抱有幻想,不放弃一点机会,不停止一日努力。"
**绪论​:
本篇文章将写道如何快速的上手Gtest和SQLite第三方库,这两个第三方库都是在项目编写过程中非常重要的。

话不多说安全带系好,发车啦(建议电脑观看) 。**


SQLite3

SQLite是⼀个进程内的轻量级数据库,它实现了⾃给⾃⾜的、⽆服务器的、零配置的、事务性的 SQL数据库引擎。它是⼀个零配置的数据库,这意味着与其他数据库不⼀样,我们不需要在系统中配置。
像其他数据库,SQLite 引擎不是⼀个独⽴的进程,可以按应⽤程序需求进⾏静态或动态连接,SQLite直接访问其存储⽂件(很多app中都是用了该数据库)

  • 不需要⼀个单独的服务器进程或操作的系统(⽆服务器的)
  • 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)中运
    ⾏(跨平台的)

常用接口:

1. 查看当前数据库在编译阶段是否启动了线程安全:

cpp 复制代码
int sqlite3_threadsafe(); 0:未启⽤; 1:启⽤
需要注意的是sqlite3是有三种安全等级的:
    1. ⾮线程安全模式
    2. 线程安全模式(不同的连接在不同的线程/进程间是安全的,即⼀个句柄不能⽤于多线程
   间)
    3. 串⾏化模式(可以在不同的线程/进程间使⽤同⼀个句柄)

2. 创建/打开数据库⽂件,并返回操作句柄

cpp 复制代码
操作句柄:ppDb
 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表⽰成功
flag(宏)选项(代表线程 安全等级): 
 SQLITE_OPEN_READWRITE -- 以可读可写⽅式打开数据库⽂件
 SQLITE_OPEN_CREATE -- 不存在数据库⽂件则创建
SQLITE_OPEN_NOMUTEX--多线程模式,只要不同的线程使⽤不同的连接即可保证线程
安全
SQLITE_OPEN_FULLMUTEX--串⾏化模式 (一般使用这个!!)


int sqlite3_exec(sqlite3*, char *sql, int (*callback)(void*,int,char**,char**), void* arg, char **err)
返回:SQLITE_OK表⽰成功/反之是出错的
传进去的回调函数:
 int (*callback)(void*,int,char**,char**)
 void* : 是设置的在回调时传⼊的arg参数
 int:⼀⾏中数据的列数
 char**:存储⼀⾏数据的字符指针数组
 char**:每⼀列的字段名称
 这个回调函数有个int返回值,成功处理的情况下必须返回0,返回⾮0会触发ABORT(异常)退出程序

3. 销毁句柄:

cpp 复制代码
 int sqlite3_close(sqlite3* db); 成功返回SQLITE_OK
 int sqlite3_close_v2(sqlite3*); 推荐使⽤--⽆论如何都会返回SQLITE_OK

4. 获取错误信息:

cpp 复制代码
const char *sqlite3_errmsg(sqlite3* db);

实操(简单封装SQLit3来使用)

创建sqlite3目录下创建sqlite.hpp文件,封装实现一个sqliteHelper类,提供简单的sqlite数据库操作接口,完成数据的几乎增删查改操作

  1. 打开/创建数据库文件
  2. 针对打开的数据库执行操作(一库一文件)
    1. 表的操作
    2. 数据的操作
  3. 关闭数据库

头文件:sqlite3.h

SqliteHelper类

  1. 构造(dbfile)
    1. 初始化_dbfile、_handler设为空
  2. bool open(safe_leve = SQLITE_OPEN_FULLMUTEX)
    1. 使用int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const
      char *zVfs );
    2. int ret接收sqlite3_open_v2返回值,设置flag:SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | safe_leve,zvfs设为nullptr
    3. 失败ret != SQLITE_OK 打印:创建/打开sqlite数据库失败: 在打印:sqlite3_errmgs中查看错误
  3. exec(sql语句,SqliteCallback cb,arg)
    1. 重命名回调函数为SqliteCallback
    2. 调用sqlite3_exec执行(错误信息填null)
    3. 同样失败(!=SQLITE_OK):打印sql、执行失败、查看错误信息
  4. void close()
    1. 调用sqlite3_close_v2

成员变量:

成员句柄 _handler

数据库文件:_dbfile

测试使用

main:

创建SqliteHelper类gelper对象

  1. 创建/打开库文件
    1. assert:open(打开失败就断言)
  2. 创建表(不存在则创建),学生信息:学号,姓名,年龄
    1. ct:sql语句 "create table if not exists student(sn int primary key ,name vchar(32),age int);";
    2. 调用exec(创建表不用回调)
  3. 新增数据,修改,删除,查询
    1. insert_sql:"insert into student values(1,小明,18),(2,'小黑',19),(3,'小红',18);";
    2. select_sql:"select sn,name,age from student"
    3. assert exec(插入语句,回调设为空)
  4. 关闭数据库 close

makefile:略(注意连接sqlite3库)

sqlite数据库的使用:

  1. 打开数据库:使用sqlite3 db文件
  2. 使用.tables 查看表
  3. 使用sql语句查看 select * from student

修改执行语句进行

修改:"update student set name='张小明' where sn = 1;";

删除:"delete from where sn = 3;"

查询:"select name from student"

  1. 创建变量arry(vector的用于储存查询到的的结果)
  2. 打印arry中的数据(循环)

不同了需要写回调函数(将数据保存到vector中):

cpp 复制代码
回调函数格式:int (*callback)(void*,int,char**,char**)
1. void* 是外部传递进来的参数(通过强转使用)
2. int 是一行中的数据列数
3. 第一个char** 是储存每一行的字符指针数组
4. 第二个char** 是存储每一列的字段名称

select_stu_callback回调函数

  1. 将arg参数强转为vector* 得到外部的容器
  2. 然后将结果(只用储存第一个char** 中的第一行数据,因为查询结果只有一个)push到vector中
  3. 返回0(不然的话会导致abort异常)必须有!

sqllite3.hpp源码:

cpp 复制代码
#include <iostream>
#include <string>
#include <sqlite3.h>
#include <vector>
#include <cassert>
#include "loger.hpp"

class  SqliteHelper{
typedef int (*SqliteCallback)(void*,int,char**,char**);

public:
    SqliteHelper(const std::string& dbfile):_dbfile(dbfile),_handler(nullptr){}


//int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs );
bool open(int safe_leve = SQLITE_OPEN_FULLMUTEX)//SQLITE_OPEN_FULLMUTEX默认就为串行化模式
{
    int ret = sqlite3_open_v2(_dbfile.c_str(),&_handler,safe_leve | SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE,nullptr);
    //第一个参数是db文件路径
    //第二个参数是一个输出型变量,我们将sqlite的句柄传进去,获取open后db的句柄
    //第三个参数是文件打开的默认权限和方式 和 线程的安全等级
    //SQLITE_OPEN_CREATE: 不存在数据库⽂件则创建
    // SQLITE_OPEN_READWRITE -- 以可读可写⽅式打开数据库⽂件
    //第四个参数设为空即可
    if(ret != SQLITE_OK)
    {
        ELOG( "创建/打开sqlite数据库失败:%s",sqlite3_errmsg(_handler));
        return false;
    }
    else{
         DLOG("打开成功");
        return true;
    }
}

//提前设置 SqliteCallback ==  int (*SqliteCallback)(void*,int,char**,char**)回调函数
bool exec(const std::string& sql,SqliteCallback cb,void* arg){
    int ret = sqlite3_exec(_handler,sql.c_str(),cb,arg,nullptr);
    //

    if(ret != SQLITE_OK)
    {
        std::cout << sql << std::endl;
        ELOG( "执行失败、查看错误信息: %s",sqlite3_errmsg(_handler));
        return false;
    }
    else{
        DLOG ("执行成功");
        return true;
    }
}

bool close(){
    sqlite3_close_v2(_handler);//
    return true;
}

private:
    std::string _dbfile;
    sqlite3 * _handler;
};

测试源码:

cpp 复制代码
#include "sqllite3.hpp"
#include <cassert>
#include <iostream>
/*
    int:一行中有的属性个数(列数)
    char** 储存一行数据的字符指针数组
    char** 每一列的字段名称
 */
 int Select(void* v,int,char** table,char** ){
    std::vector<std::string>* data = (std::vector<std::string>*) v;

    data->push_back(table[0]);//只有一个数据所以直接拿去table[0]
    return 0;//必须返回0,不然就会abort异常
 }

int main()
{
    SqliteHelper sq("./test.db");
    
    assert(sq.open());

    // sq.exec("create table if not exists student(sn int primary key,name vchar(32),age int);",nullptr,nullptr);
    
    // sq.exec("insert into student values(3,'小明',18)",nullptr,nullptr);//插入不需要有回调


    std::vector<std::string> v;
    sq.exec("select sn,name,age from student",Select,&v);

    for (auto &name : v) {
        std::cout << name << std::endl;
    }   

    sq.close();
    return 0;
}

GTest单元测试框架

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

GTest的使用

TEST的宏断言

TEST宏:

  1. TEST(test_case_name, test_name)
  2. TEST_F(test_fixture,test_name)
  • TEST:主要⽤来创建⼀个简单测试, 它定义了⼀个测试函数, 在这个函数中可以使⽤任何C++代码并且使⽤框架提供的断⾔进⾏检查

  • TEST_F:主要⽤来进⾏多样测试,适⽤于多个测试场景如果需要相同的数据配置的情况, 即相同的数据测不同的⾏为

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

  1. ASSERT_系列:如果当前点检测失败则退出当前函数
  2. EXPECT_系列:如果当前点检测失败则继续往下执⾏
cpp 复制代码
// 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

上述操作的宏 都必须在TEST测试单元中使用

实操(使用断言的宏):

所需头文件:gtest/gtest.h

cpp 复制代码
TEST(该测试名称,单元测试名称)
{
	使用宏
}

main(argc、argv)
testing::InithGoogleTest(&argc、argv)//初始化单元测试
RUN_ALL_TESTS()运行所有单元测试
  1. 使用进行大于测试:ASSERT_GT:

实操(俩种断言宏的区别):

  1. 小于测试:ASSERT_LT:

发现OK没执行。

  1. 如果使用EXPECT替换ASSERT,就会打印OK

发现OK执行了
总结:

因为断言宏的使用如下:
1. ASSERT_ 断言失败则直接退出
2. EXPECT_ 断言失败继续运行

事件机制

GTest中的事件机制是指在测试前和测试后提供给⽤⼾⾃⾏添加操作的机制,⽽且该机制也可以让同⼀测试套件下的测试⽤例共享数据。

GTest框架中事件的结构层次

  1. 测试中,可以有多个测试套件(可以理解为一组单元测试
  2. 测试套件:一个测试环境,可以在单元测试直接进行测试环境初始化,测试完毕后进行测试环境清理
  3. 全局测试套件:在整体的测试中,只会初始化一次环境,在所有测试用例完毕后,才会清理
  4. 用例测试套件:在每次的单元测试中,都会重新初始化测试环境,完毕后清理环境

全局测试套件:

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

如下:

cpp 复制代码
class TestEnv : public testing::Environment {
 public:
 virtual void SetUp() override{}
 virtual void TearDown() override{ }
};

全局测试套件,其实就是用户定义一个全局的测试环境类
SetUp:在所有单元测试运行前执行的接口,常用测试环境进行初始化(注意他们是虚函数!)
TearDown:在所有单元测试运行结束后执行的接口,用于环境清理

实操:

使用一个全局的map来测试使用:

全局初始化插入两个数据,然后再测试判断,然后删除,后再判断,来使用,查看失败的情况

全局事件类

cpp 复制代码
std::unordered_map<std::string,std::string> _dict;
class HashTest : public testing::Environment{
public:
    virtual void SetUp() override{
        std::cout << "测试前:提前准备数据" << std::endl;
        _dict.insert(std::make_pair("hello","你好"));
        _dict.insert(std::make_pair("apple","苹果"));
    }
    virtual void TearDown()override{
        std::cout << "测试结束后:清理数据" << std::endl;
        _dict.clear();
    }
};

单元测试:

cpp 复制代码
TEST(HashTest1, test1) {
    ASSERT_EQ(_dict.size(), 2);
    _dict.erase("hello");
}

TEST(HashTest2_test1_Test, test1) {
    ASSERT_EQ(_dict.size(), 2);
}

主函数

  1. 初始化单元测试InitGoogleTest
  2. 添加全局测试:testing::AddGlobalTestEnvironment(new 全局测试套件类)
  3. 开始所有测试查看情况
cpp 复制代码
int main(int argc,char* argv[])
{
    testing::InitGoogleTest(&argc,argv);
    testing::AddGlobalTestEnvironment(new HashTest);
    //调用AddGlobalTestEnvironment new 一个全局测试类
    RUN_ALL_TESTS();
    return 0;
}

分析:最终结果(因为在第一个测试后删除了一个元素,所以第二次测试时判断大小是否为2时就会报错)

用例(独立)测试事件(最重要的):

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

  1. SetUpTestCase() 函数:是在测试套件第⼀个测试⽤例开始前执⾏
  2. TearDownTestCase():函数是在测试套件最后⼀个测试⽤例结束后执⾏
  • 需要注意TEST_F的第⼀个参数是我们创建的类名,也就是当前测试套件的名称,这样在TEST_F宏的测试套件中就可以访问类中的成员了(也就是单元测试的名称 == 测试套件类名)。

具体如下图:

TestCase事件:

针对⼀个个测试⽤例。测试⽤例的事件机制的创建和测试套件的基本⼀样,不同地⽅在于测试⽤例实现的两个函数分别是 SetUp 和 TearDown , 这两个函数也不是静态函数

  1. SetUp():函数是在每⼀个测试⽤例的开始前执⾏
  2. TearDown():函数是在每⼀个测试⽤例的结束后执⾏

在TestSuite/TestCase事件中,每个测试⽤例,虽然它们同⽤同⼀个事件环境类,可以访问其中的资源(公共成员变量),但是本质上每个测试⽤例的环境都是独⽴的(也就是其成员变量的数据并不是共享的,而是独立的),这样我们就不⽤担⼼不同的测试⽤例之间会有数据上的影响了,保证所有的测试⽤例都使⽤相同的测试环境进⾏测试

实操:

独立测试事件类:

  1. 创建MyTest类,公开继承testing::Test
  2. 成员函数
    1. static void SetIpTestCase()
    2. static void TearDownTestCase()
    3. void SetUp() override
    4. void TearDown() override
  3. 成员变量(公用的)
    1. map

测试单元:

其中每一个单元测试中他们得到独立测试事件中的变量都是独立属于自己的

  1. TEST_F(MyTest,insert_test)
  2. TEST_F(MyTest,size_test)

主函数:

  1. 初始化单元测试 InitGoogleTest
  2. RUN_ALL_TESTS

具体代码:

cpp 复制代码
#include <iostream>
#include <gtest/gtest.h>
#include <unordered_map>

class MyTest : public testing::Test {
    public:
        static void SetUpTestCase() {
            std::cout << "所有单元测试前执行,初始化总环境!\n";
            //假设我有一个全局的测试数据容器,在这里插入公共的测试数据
        }
        void SetUp() override {
            //在这里插入每个单元测试所需的独立的测试数据
            std::cout << "单元测试初始化!!\n";
            _mymap.insert(std::make_pair("hello", "你好"));
            _mymap.insert(std::make_pair("bye", "再见"));
        }
        void TearDown() override {
            //在这里清理每个单元测试自己插入的数据
            _mymap.clear();
            std::cout << "单元测试清理!!\n";
        }
        static void TearDownTestCase() { 
            //清理公共的测试数据
            std::cout << "所有单元测试完毕执行,清理总环境!\n";
        }
    public:
        std::unordered_map<std::string, std::string> _mymap;
};

TEST_F(MyTest, insert_test) {
    _mymap.insert(std::make_pair("leihou", "你好"));
    ASSERT_EQ(_mymap.size(), 3);
    
}
TEST_F(MyTest, size_test) {
    ASSERT_EQ(_mymap.size(), 2);
}


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

通过对单元测试用例中的共用成员map操作,发现在第一个TEST_F中map内元素个数是3,而第二个TEST_F中map元素个数是2,都没报错,所以每个单元是独立的!


本章完。预知后事如何,暂听下回分解。

如果有任何问题欢迎讨论哈!

如果觉得这篇文章对你有所帮助的话点点赞吧!

持续更新大量C++细致内容,早关注不迷路。

相关推荐
Cooloooo22 分钟前
树的重构【东北大学oj数据结构7-4】C++
数据结构·c++·重构
梦夏夜24 分钟前
go 跨平台打包
开发语言·后端·golang
Java学长-kirito44 分钟前
springboot/ssm武理多媒体信息共享平台Java代码编写web项目视频作品分享源码
java·spring boot·后端
很楠不爱1 小时前
MySQL——连接
数据库·mysql
妍妍的宝贝1 小时前
MySQL-9.1.0 GTID模式
服务器·数据库·mysql
Code哈哈笑1 小时前
【MySQL 保姆级教学】用户管理和数据库权限(16)
数据库·mysql
振华OPPO1 小时前
SpringBoot属性配置方式
java·spring boot·后端·spring·maven·jar·配置文件
爱驹一族1 小时前
Ubuntu 20.04LTS 系统离线安装5.7.44mysql数据库
数据库·mysql
zhlh_xt2 小时前
如何查询SQL Server数据库服务器的IP地址
服务器·数据库·sql server·查询数据库服务ip地址
2401_895813642 小时前
Django ORM – 多表实例
android·数据库·django