学习软件测试时,进行了为期一个月的实验。选择要测试的项目选了很久【事实证明完美主义的结果是拖延】。在最后一周时间内,泛读了两三天+最后三天不间断的每天近12h高强度实验下,完整了覆盖率测试及报告分析。卫冕了ddl战士的称号。
在此将过程中学到的知识(主要是试错中)做个总结,确实很有收获。
1.关于头文件_头文件的重新封装
可以看到,大型功能函数的定义都近百行;因而整个.C文件近2K行,难以短时间查阅理清逻辑;
于是初步想法:函数封装到头文件里去;
常见的模式:"header.h"里,有函数a的声明; 定义在main.c(即上面的主文件)里实现;
我仍觉得量大,于是把定义与声明都放在了"header.h"里,自以为很方便;
但是会报错重定义....后来才知道不能把定义与声明都放在头文件里,这样会导致反复编译链接
(Q:如何实现函数的定义抽离出主函数??)
2.实现自动化交互--模拟终端输入:
可以用gmock。
我这里采用的是Expect-send ,可以通过代码模拟终端输入;
set timeout 1.5 设置expect每次最多等待1.5s;
spawn 启动可执行文件进程;
expect "xx" --如果遇到终端输出“xx”,那么send"123456",即在命令行终端输入123456;
最后expect eof 表示结束;
注意:如果expect 没有等到对应的输出内容,也会执行send.所以使用expect只是严格的规定了顺序和输入的节奏;
【Q:是否有分支类的写法,使得没有出现就不send?下一步学习查阅这方面】。
3.windows .vs. linux 部分字符/编码差异:
编码格式/换行格式不同 :windows下和linux下编辑的文件带有不同的换行符.通过Cat 无法查看;
【不可打印字符,故输出无法查看】
使用 cat -A a.txt 查看:
而linux下创建的文件输出同样内容,cat-A 输出查看如下:
解释:
$表示行末;^M是windows下的回车换行;
^I是linux下的制表符,即Tab;而windows下的Tab是转为四个空格;
4.实验细节_报错及解决
4.1函数问题:多个main/函数重定义
(1)关于存在多main函数入口(入口冲突)
(2)函数重定义_ [主要是头文件里定义与声明放在一起]
解决(1):指定入口函数
gtest测试文件的主函数main+项目文件的主函数main.
保留项目main,测试文件不使用main,具体实现如下:
$ gcc -testfiles -e test_3 test.c -o test.out
使用 -e 指定入口函数即可;
【参考没有了 main 函数,程序还能跑吗?_没有main函数的c++程序也可以执行-CSDN博客】
解决(2):确保每个函数在整个源文件中只定义一次
“multiple definition”(多重定义)表示列出的函数(例如 putch、getch、get1s、main 等)在编译的文件中被定义了多次。
因为有多个源文件包含全局函数定义,并且这些函数在多个文件中被定义。
方案1:单列头文件
将函数定义移动到一个单独的头文件中,并使用 inline 关键字标记那些短小且对性能关键的函数;
方案2:使用唯一性的声明:
可用1)# pragma once 表明:
一种预处理器指令,用于确保头文件只被编译一次;
当编译器遇到 #pragma once时,它会检查当前的头文件是否已经被包含过;
如果是,则忽略后续的包含操作,如果否,则继续处理该头文件。
2) #ifndef-#define-#endif【头文件保护宏】
如果该宏已经被定义过,则条件为假,预处理器会跳过后续的代码块;
如果该宏未被定义,则条件为真,预处理器会继续处理后续的代码块。
方案3:
采用方案1,2后,仍然重定义
于是将头文件部分冲突函数的实现放入对应函数文件内,与使用inline 异曲同工;
4.2不同系统(linux vs windows)字符格式问题
(1)makefile报错:没有分隔符;
(2).sh报错:解释器错误,没有那个文件或目录_ [脚本文件在windows下编辑过]
解决(1):cat -A file 查看
使用cat -A file 查看:【呈现非打印字符】【Linux/Unix 命令,^:控制字符;$:行尾】
因为是在windows和linux的sharefiles下共同传输编辑文件,所有会有编码换行格式的不兼容;
Makefile如下:
clean:
rm -rf *.gcno
rm -rf *.gcda
rm -rf *.gcov
html:
g++ -o test421_1 minitest.cpp -fprofile-arcs -ftest-coverage -lgtest -lgtest_main -pthread
./simulate_input.sh
gcov test421_1-minitest.gcda
gcovr --html-details -o ./test421/t1.html
2html:
g++ -o test421_2 test.cpp -fprofile-arcs -ftest-coverage -lgtest -lgtest_main -pthread
#./simulate_input.sh
./2simulate.sh
#./test421_2
gcov test421_2-test.gcda
gcovr --html-details -o ./test421/t2.html
#g++ -o test421_1 minitest.cpp -lgtest -lgtest_main -pthread
解决(2):删除这个字符
windows下每一行的结尾是\n\r; v.s. linux下文件的结尾是\n;
windows下编辑过的文件在linux下打开时,每一行的结尾就会多出来一个字符\r;
用cat -A yourfile时可以看到\r字符被显示为^M;
输入如下命令,删除这个字符;
sed -i 's/\r$//' create_infomember.sh
参考:
Shell脚本bash: /bin/bash^M:解释器错误: 没有那个文件或目录 -- 报错_/bin/bash 解释器错误-CSDN博客
4.3覆盖操作不当,导致覆盖信息不能生成/有误
(1)时间戳timestamp不同:
(2)关于test的函数实际覆盖与显示的覆盖行情况不一_[显示注释被执行2次]
(3)生成.gcno而不生成.gcda,且覆盖率0%
解决(1)(2):删除前一次的文件(.gcda/.gcno)
因为覆盖信息里有时间信息,在未删除前一次的覆盖信息时,无法生成新的覆盖信息;
后续发现,是前一次没有make clean 清理掉.gcov文件,导致文件冲突,会报错timestamp时间戳问题,也可能不报错,结果就是显示混乱【E.g: 显示注释处未覆盖等等】
即每次make html 前 make clean;
解决(3):正确运行完程序,不要非法终止;
-fprofile-arcs 生成.gcno;【编译时生成】
-ftest-coverage 生成.gcda;【注意:覆盖率信息是在运行程序时生成的】
故确保程序正确执行【中途交互出错则会导致失败,覆盖率为0%】(本次测试中的大部分时间花在调试此bug上)
4.4 Bash脚本报错
(0)/bin/bash^M:解释器错误[同4.2(2)]
(1)expect-send 交互失败
(2)send用法错误
解决(1):
进程已结束,expect不到对应的语句,自然无法send:可以看到是 “not open ”,即对应进程未启动;发现是交互顺序有误,导致进程提前结束,故send失败;
解决(2):
Cat -A 查看,发现把注释也同样识别为输入了,删除或换行注释即可;
4.5文件权限导致的_段错误+创建文件失败
(1)(2)问题如题目.
即访问非法内存,这里是给fopen()只读的权限,而后续有写入操作,导致访问了"不可访问的内存",报段错误.
解决(1):chmod +x minitest.cpp 【符号模式】
关于chmod:字符模式(即+x等对应权限的字母,如上)/数字模式(示下):
读取权限:4;写入权限:2;执行权限:1
读取和执行:数字模式为 5(4 + 1)。
读取、写入和执行权限:7(4 + 2 + 1)。
尾附部分实验报告。
一、实验要求
设计测试用例配合gtest测试,并使用GCOV分析一个1500行以上的真实C/C++开源程序(不能使用程序自带的测试用例!!!) 提交实验报告,内容包括:
- 1)分析过程:使用过程、编译运行命令、测试用例构建等;
- 2)分析结果:覆盖率信息
二、实验内容:
2.1实验环境:
内核及ubuntu版本如下:
2.2测试对象:
2.2.1程序的规模:
总共计数为:1650行;
【Cloc . 】查看对应代码数量如下: 实际C代码行数为1297;
2.2.2代码结构:
main函数模拟整个流程,其余为实现函数。具体见下“主要函数”。
其主函数流程如下【一个while循环下不断判断执行】:
主要函数如下:
2.3测试设计:
2.3.1编写测试用例的考虑:
分析代码,可以看到get1ch【一个输入函数】居多:
且输入输出同样居多,于是采用Bash的expect-send模拟输入输出的用户交互,
且void类函数居多,传参少,大体是用户交互及自带的txt文件【存储系统内的各类图书、用户信息】
TEST函数简便,如下:
Bash命令行交互内容复杂【共编写了约400行进行输入输出模拟】,部分如下:
最后确定了使用bash语言中的expect-send模式进行输入模拟【尝终于实现自动化测试】;
2.3.4实验流程
2.3.4.0 函数封装及文件调整(首个项目尝试过程)
注:这是第一个项目的开始流程,由于过程中的操作让我收获很多,于是同样放在此处:
第一个项目中起初得到的整个main.c文件为1853行(见图1)于是调整文件,把函数声明和定义等封装为头文件(并未更改任何代码),以便进行单元测试;(结构调整图and各种头文件内部代码如下)
图表 1 defineFunctions.h(函数定义)
2.3.4.2实现代码模拟终端输入
但是发现每次需要在终端输入,于是研究2天研究如何模拟终端输入:
最后成功,使用expect-send模式发送:
模拟终端输入(成功!):
2.3.4.3编译运行and 生成文件
封装的Makefile如下:
解释如下:
- clean:【删除已有的gcda,gcno,gcov,避免出现重复时间戳timestamp导致报错(文末第四part有记录)】
- rm -rf *.gcno: 删除所有.gcno文件。.gcno文件是由gcc在编译时创建的,它们包含了有关代码覆盖率的信息。
- rm -rf *.gcda: 删除所有.gcda文件。.gcda文件是由gcc在编译时创建的,它们包含了实际的覆盖率数据。
- rm -rf *.gcov: 删除所有.gcov文件。.gcov文件是由gcov工具创建的,它们包含了覆盖率报告的文本形式。
- 2html【同html,不过此处专门用作单元测试,便于检验各函数覆盖率及根据报错精准定位问题】:
- g++ -o test421_2 test.cpp -fprofile-arcs -ftest-coverage -lgtest -lgtest_main -pthread: 使用g++编译test.cpp文件,生成可执行文件test421_2。编译选项包括启用代码覆盖率分析(-fprofile-arcs -ftest-coverage),以及链接gtest和gtest_main库(-lgtest -lgtest_main)。-pthread选项是为了支持多线程代码。
- ./simulate_input.sh: 运行simulate_input.sh脚本来模拟终端输入。
- ./2simulate.sh: 运行2simulate.sh脚本来模拟终端输入。
- ./test421_2: 运行生成的可执行文件test421_2来执行测试。
- gcov test421_2-test.gcda: 使用gcov工具来分析.gcda文件,生成覆盖率报告。
- gcovr --html-details -o ./test421/t2.html: 使用gcovr工具来创建HTML覆盖率报告,并将其保存到./test421/t2.html文件中。
2.3.4.4部分函数【除去简单输出类】测试样例及覆盖率展示
注:大部分函数实现通过如下模式【执行函数—终端模拟交互】进行覆盖,重复性极高;故只展示部分有特殊细节的测试函数:
手动加入了输出,检测过程:
输出结果相同,即strcmp应该为0,进入对应分支,而实际没有;
发现是windows下和linux下编辑的文件带有不同的换行符:【不可打印符号,故输出无法查看】
使用 cat -A a.txt 查看:
输入: sed -i 's/\r$//' create_infomember.sh解决后:
关于expect-send模式的输入细节,在2种模拟下,发现必须expect终端输出才能对应,不然会提前输出,如下所示:【expect不到的情况】
5) void modify_book()
同【执行函数—终端模拟交互】;
与【2.5】相似,此处的strcmp同样无法返回0;
注意expect-send中的输入:
有的输入无需“\r“【或者说不能有】,否则交互过程极易出错;
且同样有理论可达但实际不能进入的分支:
2.4测试命令:
逐条解释项目构建Makefile中包装的测试命令以及命令对应的输入输出,保证结果可复现。
Makefile及命令解释,详见【2.3.4.3编译运行】部分;
关于.sh【即Bash交互】中,模拟输入输出约400行,其中代码见附件;大体框架为expect-send模拟;
测试用例约20个;近130行;主体是启动void函数及断言输出;
2.5测试结果:
2.5.1覆盖率结果:
注:有一个bug语句无法逻辑覆盖;
分支覆盖率是大部分被printf(),fclose()等基础功能函数拉低;
2.5.2未覆盖原因说明:
1)无法进入的分支:
【实际文件内,不可能没有书,即quantity永不为0】
2)printf()等函数大规模使用
一些默认库的功能函数,默认库实现时带有分支【具体见其实现如下】:
实际分支覆盖率为:705/739 = 95.4%【仍有部分个位数量级的函数分支实际不可达,此处未考虑】
3)程序质量分析:
质量存在较大问题,实际与用户交互中,问题较多,但总是以不可思议的方式继续运行;
输入错误输入,但仍然能运行【虽然是在错误处理下的继续运行,这时已经是逻辑错误了,运行下去也没有意义了】,有一定鲁棒性。
4)程序性能分析:
存在部分bug,但系统整体的鲁棒性较好,几乎覆盖了所有不同类型的输入【尤其是不合法的输入】。Bug部分示下:
潜在Bug1:跳转错误
潜在Bug2:关于分支判断条件的问题:
Check函数如下图所示:
可以看到,此中的strcmp()比较及参数带有很多问题:
1.编号是char数组,而非int类;
2.fscanf读入library.txt的图书数据,末尾没有加换行符以终止;
以上就导致,此分支永远无法进入,因为输入的number是”12\r”一类:
(WINDOWs下查看 library.txt)
(linux下cat -A 查看library.txt)
即实际的bookinfo[booknumber]是”1 ”带有很多空格而末尾不带有终止符,或是默认”\0”,导致strcmp永远不返回0,自然此分支无法覆盖;【此问题出现较多,分支覆盖达不到基本是因为此strcmp的问题】
同样,2.3.4.4_3) void delete_book() 也详细输出对比了两种情况。
改进意见:修改strcmp的定义,即自实现一个strcmp函数,忽略掉文末的不同终止符;实现正确比较。
其余见【2.3.4.4】函数测试实现中,详细记载了各类函数的bug及解决。
四、补充内容:
对源代码部分修改的说明:
1)由于输入输出极其多,为查看执行过程,将部分影响显示的清屏函数system(clear)注释掉;
2)由于部分分支是打开相应文件查看内容,总有分支是对于文件为空的情况,事实上该文本文件并不为空,实际情况下,此分支不会进入。此类分支占大多数,故分支覆盖率不高。
3)两个函数使用同一文本,而对其操作和写入的格式不一,导致此函数执行写入后下一函数打开即读取错误;此系统此错误难以解决,因为此共用文件不可缺少,唯一解决办法是修改其输入格式。
心得体会和经验收获:
算是对课上理论的一次实践。花费了很多时间,很多时间感觉是试错浪费掉了…但是不可否认的是在这些试错中学到了很多软件底层的知识,算是对很多知识的补全;在此作部分总结:
1)关于”\r”和“\n”
- \r (Carriage Return):
- 将光标移动到行首而不创建新行。
- \n (New Line):
- 在 Linux 系统中,\n用于创建新行。Linux默认行结束符。打印文本时,\n会确保文本在屏幕上创建一个新的文本行。
在不加转义符时,就会在错误组合下编码,如下:
“梦\n” 会打印为以上末尾的乱码;
2).sh模拟输入输出交互
发现此处等待很久:
查询后,设置超时时间:
3)补充
其余见【2.3.4.4部分函数测试】中,详细记载了各类函数的bug及解决。
以及见【三、各类报错及解决】中,关于试错与收获的详细记载。