gitbook/人人都能学会的编程入门课/docs/213768.md
2022-09-03 22:05:03 +08:00

209 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 26 | 牛刀小试(上):实现测试框架前的基础准备
你好,我是胡光。经历了千难万险,我们终于来到了这次编程探险旅程的最后一个阶段“综合项目篇”。
还记得开篇的时候,我跟你讲过的,要带你看看这片编程森林中最有趣的地方,不知道你感受到这份乐趣了吗?接下来的最后几节课,是我为你精心准备的,作为你编程入门学习之行的完成礼物,同时,也是对整个专栏学习内容的一个总结升华。
对于本章的学习,你需要综合运用“语言基础篇”“编码能力训练篇”以及“算法数据结构篇”中所学知识,完成两个小项目,一个是“个人测试框架开发”,另一个是“自制简易计算器”。我希望通过这两个项目,打通你编程学习的“任督二脉”,让你在后续的其他编程知识学习中,可以游刃有余,乐在其中。
## 软件开发流程
在开始做项目之前呢,让我们先来了解下一般的项目开发流程。
拿传统的软件工程开发流程来说一个软件的项目开发流程包括需求分析、概要设计、详细设计、编码、测试、软件交付、验收和维护。虽然包括了8个阶段可总的来说你可以把它总结为软件开发的前中后三个部分。
软件开发的前期,由产品经理、项目经理跟进需求,做需求分析,然后是概要设计,出一份系统的详细设计。这是项目的头部阶段,主要是从理论上验证需求可行,并且最终产出一份切实可行的软件系统的详细设计。就像我在咱们每节课中,给你留思考题,此时我的角色就是项目经理和产品经理,我首先需要分析给你留的思考题是否可做,以及跟你说明白,大致怎么做。
软件开发的中期,是由技术人员负责,主要是根据系统的详细设计,进行编码和测试,把图纸上的系统实现出来。你也可以认为这是一个施工的过程。这个过程,类比到咱们的思考题上,就是你根据我的提示,具体完成每一道思考题的程序,并且对完成的程序做简单测试,以保证程序的正确性。
软件开发的后期,由项目经理带领技术支持人员做软件交付、验收及后期维护的相关工作。这一部分,你可以理解为,是你交作业的过程,客户就是老师,你的作业要是不合格,老师就会给你退回去并提出修改建议。
从这个过程中,你可以看到作为技术人员,与我们相关的,就是软件开发的中期阶段,也就是编码和测试。
在之前的学习中,我们把学习任务主要放在了编码阶段,一直没有提测试阶段的事情,下面我们就来说说测试阶段主要做的事情。
## 黑盒测试与白盒测试
关于测试阶段的测试方法,可以大致分为五种:白盒测试,黑盒测试,灰盒测试,静态测试与动态测试。
看着这些测试方法的名称,你可能有点儿懵,其实你完全没有必要掌握全部的测试方法,只需要了解其中的黑盒测试和白盒测试就行,知道了这两种测试方法,就可以满足你对项目开发流程的概念认知。
我们先来说说白盒测试与黑盒测试都是什么意思。首先名字里面的“盒”,其实指的就是项目中的系统,你可以理解成为是我们写的程序,它也是被测试的对象。至于这个白与黑呢,意思就是在测试过程中,是否关注代码实现逻辑。
白盒测试,就是关注代码实现逻辑,从而产生的测试行为。这种行为就像你把一个盒子打开,仔细检查其内部有无错误一样。
而不关注代码实现逻辑,而产生的测试行为,就是黑盒测试。在这个测试过程中,我们只关注系统的输出是否满足我们的要求。
下面我来举几个具体的例子,你来分析一下哪一种是白盒测试,哪一种是黑盒测试。
场景一:你写完了一个程序,运行以后输入数据,测试程序的输出结果是否符合预期。
场景二:当你发现,程序的结果不符合预期的时候,开始检查你程序的代码逻辑,并且针对于每一个函数功能做测试。
在场景一中,虽然程序是你写的,可你在做测试的时候,并没有关心程序内部的代码逻辑,而是关注整体程序的功能是否符合预期,所以这个属于黑盒测试。
而在场景二中,你在测试程序的时候,关注到了程序内部的代码逻辑是否正确,并且针对代码中的函数,开始做针对性测试。由于这个测试过程关注到了代码本身的逻辑,而不单单是程序功能本身,所以,这属于白盒测试。
通过这两个日常写程序的场景,你会发现,其实测试行为对于我们来说并不陌生,测试就是为了保证程序功能的正确性的,而所谓的黑盒测试或者白盒测试,并没有优劣之分。在实际工作中,我们也会经常综合运用这两种测试,来查找程序中存在的潜在问题。
总的来说,想要保证程序的正确性,必然会涉及到运用相关的测试方法。所以请你记住,测试并不是测试人员的专属,很多时候也是开发人员需要掌握的技能。
对于开发人员来说,最基本的测试方法,就是针对自己程序中的每一个功能模块,编写对应的单元测试,而单元测试中的单元,是程序中最小的测试单位。例如 C 语言中一个单元就是一个函数C++中的一个单元,就是一个类。
所以,我们说到 C 语言的单元测试,指的是对每一个函数,编写一段测试程序。而可以辅助开发人员编写这些单元测试的程序,我们叫做“单元测试框架”,也可以简称为“测试框架”。
## 初识Google 测试框架
好了,关于软件开发流程的基本情况呢,我讲完了,下面我们正式说说测试框架。
咱们要完成的第一个项目,就是编写一个简易的 C 语言测试框架。不过在写这个C语言测试框架之前咱们先看看之前的大厂或大牛们是怎么做的这样我们在写自己的测试框架的时候也能有所借鉴。
今天,我要带你认识的是一个由 Google 开发的单元测试框架 Google Test我们一般称它为 gtest。
关于如何安装 Google 测试框架呢,由于大家的编程环境不同,所以,你可以按照网上其他资料中给出的安装教程,结合自己的编程环境,准备好 gtest 的相关环境。
gtest 是一个 C++ 的单元测试框架,如果你不方便准备 gtest 相关环境,也不碍事儿,你可以看我下面的讲解。我的环境中,已经安装好了 gtest 的相关模块,为了简单说明 gtest 的使用与运行效果,给你准备了如下代码:
```
#include <stdio.h>
#include <gtest/gtest.h>
// 判断一个数字 x 是否是素数
int is_prime(int x) {
for (int i = 2; i * i < x; i++) {
if (x % i == 0) return 0;
}
return 1;
}
// 第一个测试用例
TEST(test1, test_is_prime) {
EXPECT_EQ(is_prime(3), 1);
EXPECT_EQ(is_prime(5), 1);
EXPECT_EQ(is_prime(7), 1);
}
// 第二个测试用例
TEST(test2, test_is_prime) {
EXPECT_EQ(is_prime(4), 0);
EXPECT_EQ(is_prime(0), 0);
EXPECT_EQ(is_prime(1), 0);
}
int main() {
return RUN_ALL_TESTS();
}
```
就是一份简单的 gtest 的使用代码。代码中,包含了 gtest/gtest.h 头文件以后,程序就具备了一些魔法效果,下面让我来给你具体讲一讲。
从程序的结构上来说,先是一个 is\_prime 函数,接下来是两段以 TEST 作为开头的代码(这两段代码的作用,咱们稍后做介绍),最后是一个主函数。主函数中原本 return 0 的位置,替换成了 return RUN\_ALL\_TESTS(),也就是说,主函数中只执行了一个 RUN\_ALL\_TESTS 函数,而这个 RUN\_ALL\_TESTS 函数有什么作用呢?咱们来看一下程序的具体输出:
```
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from test_is_prime
[ RUN ] test_is_prime.test1
[ OK ] test_is_prime.test1 (1 ms)
[ RUN ] test_is_prime.test2
gtest_test.cpp:25: Failure
Expected equality of these values:
is_prime(4)
Which is: 1
0
gtest_test.cpp:26: Failure
Expected equality of these values:
is_prime(0)
Which is: 1
0
gtest_test.cpp:27: Failure
Expected equality of these values:
is_prime(1)
Which is: 1
0
[ FAILED ] test_is_prime.test2 (0 ms)
[----------] 2 tests from test_is_prime (1 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (1 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] test_is_prime.test2
1 FAILED TEST
```
由于咱们的环境有所不同,所以你环境中的输出内容,可能和我这个输出结果略有差别,不过,这不影响我们接下来的讨论。
先看输出内容的第 4 行和第 6 行,意思是说,执行测试用例 test\_is\_prime.test1 和 test\_is\_prime.test2这不就是上面两个以 TEST 开头的两段代码相关的输出内容么?
接下来从第 7 行到第 21 行是一段报错信息,意思就是说 is\_prime(4)is\_prime(0) 与 is\_prime(1) 函数返回值错误,也就意味着 is\_prime 函数实现有错误,这段错误所涉及的信息,在源代码中的第二个测试用例中有涉及。
以上就是我们对 gtest 单元测试框架的一个感性认识,从这些感性认知,我们逐渐走向理性层面,逐步展开属于我们自己的思考。
## 对于 gtest 的三个思考
面对刚才的演示代码和输出内容,你可能会产生如下三个问题:
1. 源代码中的 EXPECT\_EQ 是做什么的?
2. 以 TEST 开头的代码段,和我们学习的函数很不一样,那它究竟是什么?
3. 主函数中只调用了 RUN\_ALL\_TESTS 函数,为什么好像是执行了程序中所有的 TEST 代码段?这个功能是怎么实现的?
第一个问题不难,查看相关 gtest 的文档资料你就可以知道EXPECT\_EQ 是 gtest 里面自带的宏,主要作用是判断传入的两部分的值是否相等。如果不相等,就会产生类似于输出内容中第 7 行到第 21 行的输出内容。
第二个问题,以 TEST 开头的这段代码,明显不符合我们对 C 语言的语法认知,我们确实没有见过不用规定返回值类型,也不用规定参数类型的函数定义方式。关于 TEST 究竟是个什么的问题,更加合理的猜测,就是 TEST 实际上是一个宏。
我们来回顾一下宏的作用宏就是做简单的替换。正是因为TEST(test\_is\_prime, test1) 这段代码实际上是一个宏,所以展开以后,和后面的大括号中的内容一起组成了一段合法的代码内容,这样理解,原本的代码内容也就解释得通了。
而 TEST 宏展开以后会被替换成什么内容呢?关于这个问题,我留下充足的时间请你去思考,同时,包括第三个问题的答案,都留作今天的思考题。我希望你认真想一想,并将你的思考结果写在留言区,我们一起讨论。
下面这段代码,就是作为我们后续的项目测试代码,对于这份源代码,我们不会对它做任何的改动,所以我建议你把代码内容保存下来,以备咱们后面课程练习使用。
```
#include <stdio.h>
#include "geek_test.h" // 替换掉原 gtest/gtest.h 头文件
// 判断一个数字 x 是否是素数
int is_prime(int x) {
for (int i = 2; i * i < x; i++) {
if (x % i == 0) return 0;
}
return 1;
}
// 第一个测试用例
TEST(test1, test_is_prime) {
EXPECT_EQ(is_prime(3), 1);
EXPECT_EQ(is_prime(5), 1);
EXPECT_EQ(is_prime(7), 1);
}
// 第二个测试用例
TEST(test2, test_is_prime) {
EXPECT_EQ(is_prime(4), 0);
EXPECT_EQ(is_prime(0), 0);
EXPECT_EQ(is_prime(1), 0);
}
int main() {
return RUN_ALL_TESTS();
}
```
我们后续的目标,就是开发一个自己的测试框架,替换掉上述代码中的 gtest/gtest.h 头文件,使得程序具有和之前类似的功能和输出内容。整个过程,我们不会改动上述代码中的内容,这一点,请你一定要牢记。
从这一课开始,我希望你能脱离我的指导和提示,可以独立完成咱们每节课的任务,这也是为了检验你这段时间以来的学习成果。当然,如果你实在想不出答案,也可以回来看看我的做法,以供参考。
## 课程小结
最后呢,我来给你做一下今天的课程小结:
1. 测试行为,不是测试工程师的专属,你应该把它作为一个开发工程师的习惯。
2. 单元测试属于白盒测试范畴Google 的 gtest 就是一种辅助我们编写单元测试的框架。
3. gtest 中的 TEST 本质上是一个宏,而这个宏应该展开成怎样的代码内容,还需要你认真思考,这个思考过程对你来说是很有价值的。
好了,今天就到这里了,我是胡光,我们下期见。