gtest测试框架使用详解_SQL测试

SqlDriveUT使用说明

SqlDriveUT 功能介绍

关于单元测试(Unit Testing),业界有很多通用的测试工具,如Gtest、CppUnit、Junit等,这些工具框架的基本原理都是一致的:

  1. 框架提供一套接口,用户直接使用这些接口书写用例,进行测试时,框架会自动执行用户的用例。
  2. 程序员在用例中需要构造函数被测函数的参数,并且调用被测函数,最后检查被测函数的结果。

如下为Gtest的一个用例,Factorial是被测函数,-5、-1、10是函数参数,EXPECT_EQ检查函数的返回值是否符合预期。用户只需要写出下面的用例,运行测试时,框架会调用Factorial(-5),并且把返回值与1做对比。如果返回值等于1,则EXPECT_EQ为真。如果所有的EXPECT_EQ都为真,则这个用例通过,否则这个用例失败。

TEST(FactorialTest, Negative) {

EXPECT_EQ(1, Factorial(-5));

EXPECT_EQ(1, Factorial(-1));

EXPECT_GT(Factorial(-10), 0);

}

上面的示例中,函数的参数是-5、-1等整数,这种参数的共同点是:构造简单。假如被测函数的入参是多个复杂结构体,并且函数内部依赖很多全局变量,则构造这些入参和全局变量的代价会很大。那对于数据库系统,代码逻辑复杂,要完成以上初始化,对于部分模块代价比较高。

SqlDriveUT(Query Driven Unit Test)为高斯开发的一款新UT测试框架工具,以Query为驱动,降低函数初始化成本,可以进行UT的基本测试。

SqlDriveUT与传统的UT框架不同的是,SqlDriveUT需要整个系统正常运行。等到系统运行到被测函数时,为用户提供修改函数入参的功能。由于系统已经正常运行到被测函数,因此被测函数的参数均已经构造完毕。用户可以在此时对函数的参数做有针对性的修改。

与传统UT框架从零构造参数相比,SqlDriveUT在此时修改函数的参数,所需的工作量大幅降低,因为SqlDriveUT只需要修改此用例关心的部分;使用传统框架时,所有函数的入参均需要构造,无论入参是否与此UT用例是否有关。

目前SqlDriveUT 支持对DB数据库进行UT测试,支持的功能点包括如下:

  1. 可以测试在CN、DN执行的函数
  2. 支持用例按模块执行,即指定用例所述的模块,如sql模块、存储模块、通信模块等
  3. 支持用例的分层,即可以指定用例为Level 1、Level 2、Level 3等。
  4. 支持修改函数的入参、全局变量
  5. 支持使用SQLDRIVEUT_ASSERT_TRUE判断函数的执行结果

SqlDriveUT架构介绍

基本原理

SqlDriveUT最大的特点是支持用户修改函数的入参,这是通过实时替换被调函数实现的。最基本的原理是:在被测函数的入口处设置一个gdb的断点,当DB命中此断点时,直接jmp到一个新的函数入口,从而达到用新函数替换被测函数的目的。

考虑下面一段代码。假设func_foo为被测函数,而且假设func_foo的参数很难构造。

【重点】使用示例 重点

int main()

{

……

res = func_foo(1, 2)

……

}

int wrap_func_foo(int a, int b)

{

a = a*2;

res = func_foo(a, b)

SQLDRIVEUT_ASSERT_TRUE(res == 5)

}

在func_foo入口处设置一个临时断点(只命中一次),当系统正常运行命中此断点时,通过gdb的命令set $pc=wrap_func_foo使得系统直接去执行wrap_func_foo函数。在wrap_func_foo中修改函数的入参,并且调用func_foo(此时不再命中断点),拿到func_foo的返回值之后,可以使用SQLDRIVEUT_ASSERT_TRUE对返回值进行校验。

DB的主要功能是执行SQL语句,为了让系统运行到func_foo,需要先给DB发送一条SQL语句,因此此框架属于基于SQL驱动的单元测试框架。

【重点】用例实现

用例实现指的是实现一个wrapper函数,此函数完成参数修改、调用被测函数、检查被测函数返回值等功能。一个函数的示例如下。

bool

wrap_lazyagg_check_parentquery_feasibility(Query *parentParse, Index *targetRTEIndex, lazyagg_query_context *parentContext)

{

parentParse->hasAggs = NULL;

parentParse->groupClause = NIL;

bool ret = lazyagg_check_parentquery_feasibility(parentParse, targetRTEIndex, parentContext);

SQLDRIVEUT_ASSERT_TRUE(ret == false);

ereport(ERROR, (errmsg("[SQLDUT] Finish Normally.")));

}

wrap_lazyagg_check_parentquery_feasibility与函数lazyagg_check_parentquery_feasibility的参数需要完全相同。wrap_lazyagg_check_parentquery_feasibility对函数的参数做了一些修改,调用lazyagg_check_parentquery_feasibility之后,使用XY_ASSERT_TRUE检查函数的结果。由于完成此次测试之后,参数被改坏,如果继续执行,可能会报错,因此直接调用ereport(ERROR)停止执行此SQL语句。

Wrapper函数的代码在Code/src/test/SqlDriveUT/test/wrapper目录下,每个测试文件所在的子目录位置,与源代码的目录对应。比如Code/src/backend/optimizer/prep/prepnonjointree.cpp 对应测试文件Code/src/test/SqlDriveUT/test/wrapper/backend/optimizer/prep/prepnonjointree.cpp。在Code/src/test/SqlDriveUT/test/wrapper中添加测试文件,需要修改或添加新的Makefile。添加方法可参考已有的测试用例。

架构介绍

ee754641d25beba50e7dc667e2ae587f.png

SqlDriveUT的整体架构如上图。

DB、SqlDriveUT、GDB、gsql是4种不同的进程:

  1. DB进程中包含了wrapper函数(如上一节的wrap_func_foo)的代码。
  2. SqlDriveUT进程是整个测试流程的指挥中心,负责读取用例配置、指挥gdb给DB打断点、读取gsql的输出、分析用例的执行结果等。
  3. GDB进程负责给DB打断点,让DB在执行fun_foo时跳转到wrap_func_foo函数
  4. Gsql进程负责给DB发送SQL语句,并读取DB的输出结果。

SqlDriveUT在执行时,首先读取用例的配置信息,如被测函数名称、wrapper函数名称、使DB执行到被测函数的SQL语句、用例的模块、用例的Level等。SqlDriveUT根据用户的要求,选择需要执行的用例模块、用例Level,开始执行用例,主要步骤有:

  1. SqlDriveUT通过gdb给DB在被测函数(如func_foo)设置断点B,并让DB继续执行
  2. 通过Gsql给DB发送SQL语句,使DB运行到断点(func_foo)处
  3. 给gdb下发set $pc=wrap_func_foo,然后继续执行DB,从而使得DB执行wrap_func_foo
  4. SqlDriveUT读取gsql的输出,即可读取到wrapper函数中SQLDRIVEUT_ASSERT_TRUE的结果,根据这些结果判定用例是否执行成功。

SqlDriveUT使用介绍

使用流程概述

根据SqlDriveUT的架构及执行流程易知,使用SqlDriveUT进行单元测试,需要五个步骤:编写wrapper函数、编写SqlDriveUT用例、编译带wrapper函数的DB、运行SqlDriveUT进行测试、分析SqlDriveUT的执行结果。

编写Wrapper函数

Wrapper函数的作用有三个,一是修改被测函数的入参,二是调用被测函数,三是判断被测函数的返回结果是否符合预期。实现wrapper函数时需要注意几个事项。

函数签名:由于DB会从被测函数直接jmp到wrapper函数,因此wrapper函数的签名需要与被测函数完全相同,即函数的入参的个数、类型、顺序需要与被测函数完全吻合。

存放位置:Wrapper函数存放在Code/src/test/SqlDriveUT/test/wrapper下,此目录下的目录层次与DB内核目录层次相同。请将wrapper文件放在相应的目录中。

Mafefile:如果需要添加新文件/目录,需要修改/添加相应的makefile,参考已有代码修改/添加即可。

  1. 每个文件夹下都需要有一个makefile文件,添加新文件夹时,需要在父文件夹makefile中SUBDIRS中添加新文件夹名字、并且在新文件夹中添加makefile文件(详细请参见已有代码)。
  2. 添加新文件时,需要在本目录下的makefile中OBJS后添加新文件名称(详细请参见已有代码)。

头文件:Wrapper函数所在的文件中,需要加入wrapper函数所依赖的头文件。包括:

  1. Wrapper函数依赖的内核头文件。
  2. #include "utils/SqlDriveUT_assert.h",以便wrapper函数使用SQLDRIVEUT_ASSERT_TRUE等用作断言的宏。

报错处理:建议wrapper函数完成SQLDRIVEUT_ASSERT_TRUE判断之后,使用ereport(ERROR)中断SQL语句的执行流程,节省资源。

  • 如果此用例可能会破坏数据库的一致性,则必须进行报错处理,以免影响后续用例。

bool

wrap_lazyagg_check_parentquery_feasibility(Query *parentParse, Index *targetRTEIndex, lazyagg_query_context *parentContext)

{

parentParse->hasAggs = NULL;

parentParse->groupClause = NIL;

bool ret = lazyagg_check_parentquery_feasibility(parentParse, targetRTEIndex, parentContext);

SQLDRIVEUT_ASSERT_TRUE(ret == false);

ereport(ERROR, (errmsg("[SQLDRIVEUT] Finish Normally.")));

}

上面是一个wrapper函数的示例。Wrapper函数首先对参数进行一些修改,然后调用被测函数,最后使用SQLDRIVEUT_ASSERT_TRUE检查被测函数的返回结果。由于此用例把参数修改之后,此SQL语句无法正常运行,因此在用例的结尾使用ereport(ERROR)退出服务线程。

SqlDriveUT的用例的实现

从架构中可以看出SqlDriveUT是一个独立的可执行程序,它的作用是控制gsql给DB发送SQL语句,控制gdb给DB设置断点,控制DB执行wrapper函数,判断用例的执行结果等。因此,SqlDriveUT这个可执行程序需要知道触发每个用例所适用的SQL语句、被测试函数的名称(如func_foo)、wrapper函数的名称(如func_foo_wrapper)等。

这些配置项都会作为C++中类的成员实现,QTEST(xxx)会定义一个描述测试用例的类,这个类的成员变量表示这个用例的相关配置。配置项与类成员的对应关系如下表。

配置项

类的成员名称

前置操作,如创建表,导入数据等SQL语句

m_prepare_sql

触发被测函数断点的SQL语句

m_driven_sql

后置操作,如删除表等清理操作

m_post_sql

被测试的函数名称

m_test_unit

对应的wrapper函数名称

m_wrap_unit

用例所属的模块

m_moudle

用例所属的Level

m_level

执行用例的节点类型(CN、DN)

m_nodetype

下面是Code/src/test/SqlDriveUT/test/utcase/backend/commands/analyze.cpp中的部分代码。这是一个完整的用例,供给SqlDriveUT执行所使用。

#include "stdio.h"

#include "kernel/SqlDriveUT_main.h"

#include "utils/utils.h"

QTEST(get_total_width_1)

{

m_prepare_sql = "timing onn"

"create table t(a int, b int);"

"insert into t values(1, 2);";

m_driven_sql = "analyze t;";

m_post_sql = "drop table t;";

m_test_unit = "get_total_width";

m_wrap_unit = "wrap_get_total_width";

m_moudle = TEST_MODULE_SQL;

m_level = TEST_LEVEL_2;

m_nodetype = TEST_NODETYPE_DN;

}

为了对大量的用例进行管理,SqlDriveUT支持对用例按模块、层次进行划分。

目前支持的模块包括:

TEST_MODULE_BASE

TEST_MODULE_SQL

TEST_MODULE_STORAGE

TEST_MODULE_CBB

TEST_MODULE_CM

TEST_MODULE_COMM

TEST_MODULE_SECURITY

支持的层次包括:

TEST_LEVEL_0 ???

TEST_LEVEL_1 ???

TEST_LEVEL_2 ???

Makefile:

把用例文件添加到Code/src/test/SqlDriveUT/test/utcase下的某个子文件夹中即可完成新用例的添加,不需要添加/修改makefile。现有的makfile会自动搜索该文件夹下的所有cpp文件。

编译DB

对DB进行configure时需要启用--enable-SqlDriveUT选项,才能将wrapper函数编译进DB。

make distclean -s -j48

./configure CC=g++ CFLAGS='-O0 -ggdb3' --prefix=/your/path --without-zlib --enable-debug --enable-cassert --enable-SqlDriveUT

make install -s -j48;

注意事项:请使用debug版本进行测试,即启用--enable-debug

执行SqlDriveUT测试

测过过程共分为两个子步骤,一是启动带有wrapper函数的DB集群,二是执行SqlDriveUT程序开始测试。

SqlDriveUT选项介绍

SqlDriveUT程序支持下列选项,执行./SqlDriveUT即可查看

必须指定的选项

  1. –cp DB集群中CN的端口
  2. –dp DB集群中DN的端口

可选的选项

  1. –m 需要执行的用例模块列表,各个模块以逗号分隔。默认执行所有模块的用例
  2. –l 需要执行的用例Level列表,各个Level以逗号分隔。默认执行所有Level的用例
  3. –node 需要执行用例的节点类型。默认执行CN、DN上的所有用例。
  4. –logdir SqlDriveUT的日志目录,默认为当前工作目录的log文件夹

SqlDriveUT执行方式

DB的开发人员可以使用两种方式启动SqlDriveUT进行SqlDriveUT测试。

第一种方式,需要开发人员自己编译SqlDriveUT、启动集群、运行SqlDriveUT。这种方式的优点是:很快就可以出结果。

  1. 启动DB集群。
  2. 编译、运行SqlDriveUT:

cd Code/src/test/SqlDriveUT/src && make && ./SqlDriveUT –cp cn_port –dp dn_port

      • cn_port指某个CN的端口,dn_port指某个DN的端口
  1. 日志在当前工作目录下,即Code/src/test/SqlDriveUT/src/log

第二种方式,像运行fastcheck一样,直接运行make SqlDriveUTcheck p=xxxx即可。这条命令会集成编译SqlDriveUT、启动集群、运行测试。日志在Code/src/test/SqlDriveUT/log下。这种方式启动集群的速度较慢。

建议开发人员在开发阶段使用第一种方式。在特性代码合入主线前使用第二种方式。

两种方式启动的集群,某些参数可能不同,暂时以fastcheck集群中的参数为准。参数配置不同,DB走的代码逻辑可能不同,可能导致某些代码无法测试。

make SqlDriveUTcheck并不执行fastcheck中的用例,SqlDriveUTcheck与fastcheck不能同时执行。

结果分析

手工执行SqlDriveUT时,如果不指定SqlDriveUT日志的目录,则SqlDriveUT会把日志存放在当前目录下的log文件夹内。

使用make SqlDriveUTcheck的方式执行时,日志会存放在SqlDriveUT的结果在Code/src/test/SqlDriveUT/test/log中。out_resultxxxxxxx.log中可以看出用例的执行的结果。从结果中可以看出用例总数,成功的用例总数,失败的用例等信息。

[ info ] [ 10-30 10:26:04 ] ============ testcases start run =================

[ info ] [ 10-30 10:26:04 ] [info] <cn> SqlDriveUT start run cn testcases.

[ info ] [ 10-30 10:26:09 ] [SQLDRIVEUT] lazyagg_check_parentquery_feasibility_1 ...PASSED TIME TAKEN:[0.550]

[ info ] [ 10-30 10:26:09 ] [info] <cn> SqlDriveUT end run cn testcases.

[ info ] [ 10-30 10:26:09 ] [info] <dn> SqlDriveUT start run dn testcases.

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] get_total_width_1 ...PASSED TIME TAKEN:[0.250]

[ info ] [ 10-30 10:26:14 ] [info] <dn> SqlDriveUT end run dn testcases.

[ info ] [ 10-30 10:26:14 ] ============ testcases run finished =================

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Total testcase <2>

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Skipped testcase <0>

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Actual testcase <2>

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Runned testcase <2>

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Passed testcase <2>

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Failed testcase <0>

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Abnormal testcase <0>

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] All cost time <9.600000 s>

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Run module:base,sql,storage,cbb,cm,comm,security,

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Run level:0,1,2,

[ info ] [ 10-30 10:26:14 ] [SQLDRIVEUT] Run node:all,cn,dn,

上面的内容节选自out_resultxxxxxxx.log,从中可以看出一共执行了两个用例,均执行成功。两个用例的名称分别为lazyagg_check_parentquery_feasibility_1、get_total_width_1。

更详细的日志信息可以在out_resultxxxxx.log中查看。

注:

  1. 用例中的SQL语句仅仅是为了让DB运行到被测函数单元测试的结果
  2. 单元测试只关心被测函数的返回值,即wrap_func中SQLDRIVEUT_ASSERT_TRUE是否成立
  3. 不关心SQL语句的运行结果

用例失败的常见错误:

  1. 没有把wrapper函数编译进DB。请检查nm db | grep SqlDriveUT的输出,如果输出为空,则说明没有DB没有包含wrapper函数。Configure时请启用—enable-SqlDriveUT选项。
  2. 集群状态异常,如果使用kill -9的方式结束gdb可能导致DB异常。请重启集群测试。
  3. GDB版本过低,请升级gdb
  4. Wrapper函数实现错误:

请重点检查相关的SQL语句。

SqlDriveUT改进点

由于SqlDriveUT现有的架构所限制,存在一些使用约束,包括:

  1. 不支持测试DN与DN连接时执行的函数。Gsql连接CN之后,CN会有一个服务线程CN_T与此gsql对应,如果被测函数需要在CN执行,则必须在CN_T线程内执行,才能被测试。如果被测的函数需要在DN执行,则CN_T会与DN进行连接,DN会启动一个线程DN_T与CN_T对应,被测函数只能在DN_T执行才能被测试。不支持测试在DN其它线程中执行的函数。
  2. 暂时不支持mock。
  3. 暂时不支持安全认证等登陆前验证的函数接口。

除了上面的使用约束之外,还有一些改进点,需要在后续版本中解决,包括:

  1. 断点需要命中N次
  2. Make SqlDriveUTcheck启动集群太慢
  3. 所有elog都会被框架匹配,可能存在错误匹配的情况,可以通过新加日志级别解决。
  4. 对于递归函数,暂时只支持第一次替换。
  5. 暂时不支持测试C++函数
  6. 对于异常场景,被测函数在反正结果之前,已经报错退出,则无法判断结果,暂不支持这种场景。

FAQ

  1. 如何确认wrapper函数是否被编译到DB中?

nm Code/src/backend/db | grep SqlDriveUT_case_number,或grep相应的wrapper函数名称。

  1. 在用例中如何切换数据库?

“c postgres n”

  1. 如果测试失败,如何重启测试?

重启集群,重新执行SqlDriveUT

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值