撰写了文章 更新于 2020-02-09 00:00:09
《程序编写 编程入门 - 测试与调试》编写测试/认识调试器
测试
软件的测试实际上是一个极其必要的技巧,我是在学习编程的第二年才发现这个步骤的重要性,在此之前我一直使用一种更为麻烦的方法手动调试各种程序。索性大学期间并没有太复杂的项目。
我在另一系列文章中提到过一点,我并不是游戏程序员,也不是软件程序员,我是一名硬件程序员,这使得我至今依然使用一套简单的、我自己编写的,可用于硬件平台的测试框架,也因此,我并没有更多的高级测试经验。但另外一方面,这个框架完美的代替了我的毕业设计,所以我对测试框架还是拥有一套比较完善的理解,虽然如此,在这篇文章中不免会有一些错误,欢迎提出/指出,我会尽快修改。
基本结构
测试结构是独立于main程序之外的,也因此,测试结构实际上对于单文件的源代码会更加的复杂。
#include <cstdio>
#define TEST
int max(int a, int b){
return a>b? a: b;
}
void main(){
int a, b;
//公共代码,声明、模块初始化
#ifdef TEST
//测试代码
#else
//实际功能代码
scanf("%d %d", &a, &b);
printf("%d", max(a, b));
getchar();
#endif
//公共代码,例如关闭文件、释放内存
return 0;
}
这是一个简单的,输入a、b,并通过max函数来获取两个数之中最大值,但我们通过ifdef语句对TEST进行判断,如果定义了TEST则编译测试代码,否则编译实际代码。
#ifdef是编译器宏,因此,其判断是发生在编译过程之中的,因此就算如此编写,TEST的代码并不会遗留在真正的成品程序之中。
下面我们针对这个max函数,我们开始编写关于这个max函数的测试
编写测试
何时编写测试
- 先编写代码
- 功能代码较短
- 函数功能不明确
- 代码功能在后续开发中会大部分修改
- 先编写测试
- 功能代码冗长且复杂
- 函数功能明确且清晰
- 函数功能具有不安全因素
- 编程者不熟悉某个算法,可预见需要不断的细小修改
测试样例
函数测试,其本质上编程者向函数套用一系列设计好的“题目”,并将函数运行结果与预期结果进行对比,来判断函数功能是否正常。
而这些 题目-答案 的配对,被称作测试样例。
对于我们的 max()函数,我们预期三种情况,分别是 a>b, a<b, a=b 三种情况
根据我们已有知识,我们首先使用if进行判断
if(!(max(2,3) == 3)){
puts("测试失败 max(2,3)");
}else{
puts("测试成功 max(2,3)");
}
if(!(max(3,2) == 3)){
puts("测试失败 max(3,2)");
}else{
puts("测试成功 max(3,2)");
}
if(!(max(3,3) == 3)){
puts("测试失败 max(3,3)");
}else{
puts("测试成功 max(3,3)");
}
这是一段最基本的测试代码,在我们执行之后会显示三个检测通过的正确信息
断言
如果按照如此的测试代码编写,我们可以预见到,对于每个测试3-5个样例,在一个拥有10个功能性单元的微型程序中,都会使用至少30个if-else结构,这会使得代码杂乱程度暴增。
而C语言,提供了一种“断言”机制,他处在 assert.h头文件中,你也可以在c++中使用 cassert来使用这个机制。
其结构很简单
assert(条件);
条件为一表达式,当其为假时,断言会终止程序运行,并指出自身所在位置,表示此处未通过断言测试。
另外,你可以方便的在 #include <cassert\>前加入#define NDEBUG 来禁止断言,这样会让文件中的所有断言失效,不会有任何反馈
assert(max(1,2) == 2);
assert(max(2,1) == 2);
assert(max(2,2) == 2);
测试框架
除了C标准提供的这种断言方式,有更多的单元测试框架用以方便为自己的代码部署,例如CuTest(非常微型的一套断言工具)、Gtest(谷歌提供的框架)、TestNgpp(一款功能强大的测试框架)
如果你有志向独立或者参与开发一套大型的软件,你应该至少了解这些框架中的某一个,磨刀不误砍柴工,这是值得的。
如果你使用vs作为开发平台,你可以在
Visual Studio 中的测试工具
找到微软官方对于这些框架的说明和安装方法
调试
基于gdb的调试
如果你使用了vs作为你的开发环境,你可以跳过这节,并查看下一节来认识vs的调试环境
我们需要使用 g属性来使得执行文件可调式
g++ -g main.cpp -o main
并通过
gdb main
开始调试
命令 | 全称 | 含义 |
---|---|---|
运行控制 | ||
r | run | 开始程序运行,直到遇到断点 |
c | continue | 继续运行,直到遇到断点 |
n | next | 单步通过,运行下一条语句,遇到函数执行但不进入 |
s | step | 单步进入,运行下一条语句,遇到函数进入 |
fin | finish | 运行直到函数完成(返回),并展示返回点 |
u | until | 运行直到离开循环 |
call <函数> | 调用函数 | |
断点控制 | ||
b | break | 断点 |
b <函数名> | 断点与进入函数时 | |
b <行号> | 在指定行号断点 | |
b ±<行号> | 在数行前/后下断点 | |
b 文件名:行号 | 在指定文件的指定行断点 | |
b 文件名:函数名 | 在指定文件的指定函数断点 | |
b *<地址> | 在内存地址处指定断点 | |
b <> if <条件> | 当if条件符合时断点 | |
d | delete | 删除 |
delete breakpoints 删除所有断点 | ||
disable | 关闭断点 | |
enable | 打开断点 | |
watch <变量> | 当变量内容发生变化时中断 | |
数据查看 | ||
p | 查看数据 | |
display | 当程序断点时查看数据 | |
l <> | list | 查看 空(当前行)/行/函数 源码 |
whatis <> | 查看 变量/函数 信息 | |
状态查看 | ||
i | info | 查询程序状态,拥有多个子命令 |
bt | 查询调用栈 |
相对于vs提供了大量的UI与按钮,用以控制程序运行或断点设置,而相对于这些UI,你在gdb里只能使用对应的指令来完成一系列工作,当你熟识了这些指令,这并不难但其相对于vs即开即用的便利,增加了相当的学习成本。
基于vs调试器的调试
你可以在 Visual Studio 调试程序文档 找到微软官方对vs调试器的介绍
如何调试
我相信任何有经验的程序员对于下断点的方式和方法都有不同的理解,一言以蔽之,就是大量的写代码,然后踩进自己代码的陷阱里所总结出的经验,但至少有相当一部分是共通的。
- 如果你在使用一个你刚学到/刚想到的复杂算法,立刻在算法的起始位置下断点
- 如果你在代码过程中已经发觉自己的思想走上了一条不归路,留下备注,在优化代码的时候对这部分断点
- 使用一个(由于其手册书写不明/语言不同难以详细理解)的高级函数,在其后下断点,你可以在调试开始时确认函数的功能是否与自己理解相同
从此开始,你可能需要非常非常大量的断点调试,并借此来印证自己对函数/语法所不清楚的地方,并坚定自己对程序逻辑的理解有没有偏差。
但对于非编程阶段的调试,则更具有挑战性。但这种挑战性是来自于对其他程序员思想的理解上的差异所造成的。
如果你的课题小程序出现了错误,看看你的代码,试着用用调试器看,能否解决你的课题小程序。
我们接下来会通过《指针、结构体、内存》和《数据结构-顺序表之链式表》进入到一个全新的学习篇章,在这些内容中,将会有大量的调试内容,届时我会针对各种情况分享自己的调试技巧。
目录