摘要:本文介绍了一种针对关系型数据库的复杂计算代码逻辑测试方法,该方法借助关系型数据库提供的存储过程服务,可以在不依赖代码逻辑的情况下,直接编写自动验证用例,并保存整个计算流程的中间计算结果,提供简单可依赖运算正确性质量保证。
1 引言
在许多报表和统计类产品中,存在大量的复杂计算功能,这些公式往往有数十步的计算过程,上万条数据统计记录,以及跨多个关系型数据库的数据依赖,甚至是多种语言的混合实现。更棘手的是:往往为了节省数据存储资源,数据库不存储这些复杂计算逻辑的中间结果,而是直接存储最终计算结果,为前端提供数据。
在运营产品“盘古”系统中,为了衡量一个销售人员近期的业绩情况,要经过一系列的统计和计算为每个销售人员打一个综合评分,根据综合评分对销售人员分配销售任务,整个公式如下(具体业务参数和公式内涵与本文主题无关,不再冗述):
上面列举的计算还不包括大量的查询、排序、求最大值和最小值、求平均值等逻辑运算。在持久层的设计中,从原始的数据记录到最终的得分,中间没有存储任何的计算过程中间值,这点设计上没有问题,任何数据型系统都应该在不影响性能的情况下尽量节约存储空间。但是,复杂的运算导致了功能实现的软件质量风险,也给QA设计相应的测试用例带来的一定困难,下文将详细介绍,该类问题的测试解决方案。
2 针对复杂计算功能的测试方法
对于此类复杂计算的功能测试,一般采用以下两种方式进行测试:
(一) 手工用例核算的测试方法。账单、财务报表等系统的测试,QA很多采用这种手工核算的测试方法,对每一个计算项目进行手工计算核对,以此来保证代码逻辑的计算正确性。但是这种愚公移山的办法,用在上述这种没有计算过程中间值的情况下,就十分不可靠而又低效了。第一,对于万级别以上的数据查询、排序,求最大值、最小值和平均值都是不可能完成的任务;第二,即使我们结合SQL语句进行查询、排序等逻辑运算,计算结果经过上述10个公式的手工计算难以保证用例计算的正确性;第三,手工计算效率太低且成功率没有保障,每次升级后,回归测试人力开销太大。
(二) 基于单元测试框架的测试方法。这种方法是对各个逻辑接口进行插桩,根据条件分支通过等价类划分和排列组合设计各种条件组合用例来进行测试。这种方式复用性强,用例可长期维护和自动化回归测试,是种较好的解决方案。但是,对于一些数据依赖性强的系统,需要大量准备测试数据,且这些数据互相之间有逻辑关联,数据构造带来了很大的工作量,并且还要对数据本身进行验证,降低了测试效率。
3基于存储过程的测试方法
上述两种方案为目前针对复杂计算功能所最常采用的三种方法,各有长短优劣。在盘古系统相关模块的测试中,结合了手工核算的手工用例和基于单元测试框架的自动化用例的测试思想,采用了一种基于存储过程的自动测试结合手工核算的方法,实现的基本思路如下:
Step 1. 将复杂的数学计算逻辑拆分成若干个计算片段,保证每个计算片段都可以较容易的计算出来,每个计算片段生成一个中间值。
Step 2. 在测试环境中,如表1所示,构建一张测试数据表,除ID外,为每个计算片段设置一个表字段,用于存储每个记录的计算片段中间值。
Step 3. 按照Step 1拆分的计算片段逻辑,直接使用数据库存储过程实现整个计算过程,数据查询、排序,求最大值、最小值和平均值等运算都可以方便的使用数据库提供的接口,且存储过程支持变量和游标,逻辑实现起来十分简便。
Step 4. 测试执行时,流程如图1所示,先执行被测功能代码逻辑,再基于同一个数据源执行存储过程的测试用例,最后对比测试表最终计算结果和被测功能代码逻辑的计算结果。如果出现测试用例和代码逻辑计算不一致的情况,QA可根据计算过程,方便的进行手工核对,定位出错点时测试用例还是代码逻辑。若用例出错,及时修正测试用例;若测试用例没有出错,则可以根据计算过程的中间值,结合debug工具,定位代码逻辑的bug点。
这种基于存储过程的测试方式总结下来有以下优势:
(一) 与基于单元测试框架的方法相比,实现起来简单、轻量、高效。直接面向数据库进行操作,不依赖原有代码逻辑和第三方框架,开发用例代码量小,。
(二) 在测试表中留下计算全过程,方便错误的跟踪。
(三) 与手工核算的方法相比,本测试方法QA时间开销主要在测试设计阶段,测试执行时间短,且便于维护,方便自动化回归测试。
由于盘古项目的计算流程较为复杂且需要许多业务相关知识,这里为举一个简化的模拟场景来说明本文所述基于存储过程的测试方法。表2为原始记录数据表(tb_os_log),记录了销售人员工作拜访日志,
表3为销售人员得分表(tb_os_point),这个表已经预先存入参加评分的销售ID,
下面两个公式(5)是从原始记录到实际得分的计算公式,
其中, 为在 到 时间段内销售人员拜访任务总量, 为销售人员的成交订单数量,
为转化率, 为销售人员中的最高转化率, 为销售人员中的最低转化率, 为最终销售人员的得分。被测计算功能是将tb_os_log通过上述数学计算得到结果,并存入tb_os_point表的total_point字段。
测试时,首先设计并创建测试表(tb_os_test),表4为测试表的各字段说明,is_pass字段为0本条数据通过测试,is_pass字段为1本条数据测试不通过。
然后,使用存储过程编写测试用例,下面为存储过程代码
CREATE PROCEDURE
testStat(IN start_date DATE, IN end_date DATE,
IN error_allow DOUBLE)
//三个输入分别为起始日期、截止日期和允许计算误差
BEGIN
DECLARE uc_id INT DEFAULT 0;//销售人员ID
DECLARE done_num INT DEFAULT 0;//成交客户数
DECLARE do_num INT DEFAULT 0;//拜访客户数
DECLARE ratio DOUBLE DEFAULT 0;//转化率
DECLARE r_max DOUBLE DEFAULT 0;//销售人员中的最大转化率
DECLARE r_min DOUBLE DEFAULT 100;//销售人员中的最大转化率
DECLARE point_test DOUBLE DEFAULT 0;//测试代码计算逻辑
DECLARE point_tested DOUBLE DEFAULT 0;//被测功能逻辑的计算结果
DECLARE point_error DOUBLE DEFAULT 0.1;//误差
DECLARE uc_info CURSOR for SELECT ucid FROM
tb_os_point;
DECLARE CONTINUE HANDLER FOR SQLSTATE ’02000′
SET uc_info=NULL;
OPEN uc_info;
FETCH uc_info INTO uc_id;
WHILE uc_id IS NOT NULL DO//计算转化率,并保留中间过程
SELECT count(*) INTO do_num //计算总拜访数
FROM tb_os_log
WHERE visit_date>start_date AND vistit_date
SELECT count(*) INTO done_num //计算成交订单数
FROM tb_os_log ;
WHERE visit_date>start_date AND vistit_date
SET ratio = done_num / do_num;
IF ratio > r_max THEN
SET r_max=ratio;
END IF
IF ratio < r_min THEN
SET r_min=ratio;
END IF
INSERT INTO tb_os_test(ucid,os_do,os_done,os_ratio)
VALUES(uc_id,do_num,done_num,ratio);
FETCH uc_info INTO uc_id;
END WHILE
CLOSE uc_info;
UPDATE tb_os_test SET ratio_max=r_max;//在测试表中,保留最大转化率
UPDATE tb_os_test SET ratio_min=r_min;//在测试表中,保留最小转化率
OPEN uc_info;
WHILE uc_id IS NOT NULL DO//计算结果,并验证被测计算功能的计算结果
SELECT os_ratio INTO ratio FROM tb_os_test
WHERE ucid=uc_id;
SELECT total INTO point_tested
FROM tb_os_point
WHERE ucid=uc_id;//取出被测计算功能结果
SET point_test = (ratio-r_min)/(r_max-r_min)*100;//得到最终评分
UPDATE tb_os_test SET os_point=point_test WHERE
ucid=uc_id;
SET point_error = point_test-point_tested;//两个结果的误差
IF ABS(point_error) <= error_allow THEN
// 如果计算结果误差小于允许误差,则通过测试;否则,测试不通过
//标记通过测试
UPDATE tb_os_test SET is_pass=0 WHERE ucid=uc_id;
ELSE
//标记不通过测试
UPDATE tb_os_test SET is_pass=0 WHERE ucid=uc_id;
END IF
FETCH uc_info INTO uc_id;
END WHILE
CLOSE uc_info;
END
|
在测试这种复杂数学运算的功能时,有一点要特别注意,就是计算过程中的小数点取舍问题。设计和编写测试用例前,一定要和RD、PM沟通好浮点数运算的精度和取舍方法。为了防止不同语言在浮点数精度上处理方式不同而导致的误差,上面的用例还特别设计了误差阈值,在测试时,可以根据不同情况灵活处理。完成测试用例编写后,要在测试环境的数据库中创建测试所用的存储过程。
测试执行时,先要调用计算功能的接口,然后登录数据库,执行call testStat(
, ,0.1)。存储过程执行后,查看测试表中的is_pass字段即可获知,哪些数据项目测试通过,哪些数据项目测试没有通过。如果存在不通过的情况,可以根据其他字段顺藤摸瓜,很方便的定位到Bug。
4 总结
在采用本文所述方法的项目测试中,也发现了一些问题和有待改善之处:首先,一般的关系型数据库不支持存储过程调试,在测试用例调试时,必须借助第三方的调试工具,盘古项目采用的是MySQL数据库,这里推荐一个MySQL的第三方开发的调试工具dbForge
Studio for MySQL,其他数据库也有类似的调试工具);其次,不支持跨物理数据库的计算逻辑测试,目前对于跨数据库的测试,还是将多个物理库的数据拷入一个物理库的多个逻辑数据库中进行测试;最后,目前执行时需要手动启动执行,自动执行方案还需要进一步调研,可选方案有设置触发器执行、定时脚本执行等。上述都是要在今后的测试工作中跟进解决和提高的地方。
|