测试之道:开发测试管理、质量、分层与有效性
小引
开发者测试是软件质量的守护者。其基本内容包含单元测试,集成测试,以及子系统之间的跨领域测试。开发者测试有时候也被称为白盒测试,底层测试。
本文我们会从如下几个方面谈一谈测试技术:
1. 开发者测试的管理。
2. 微软如何判断测试代码的质量?
3. 微软对测试分层是如何做的?
4. 开发者测试做不好的原因是什么?
5. 开发者测试的覆盖率高低的意义,为什么有的开发者测试发现不了问题?
1. 开发者测试的管理
开发者测试一般来说包括单元测试,功能集成测试和子系统之间的测试。
单元测试是针对函数这一层级的测试,一个文件中会存放多个函数,我们在创建测试文件的时候,我们会对应这个源代码的文件。比方说我们现在有个文件叫source1.xx,那我们在创建测试文件的时候,我们可以定名为source1Test.xx,接下来,我们会把对应的测试放到这个测试文件当中。
针对一个函数,我们有时候会有多个测试案例,我们要做的就是把这一些针对一个函数的测试案例放在一起。比如说我们现在有个函数function1, 还有多个输入参数,有一个返回值。我们可以创建如下的测试案例: function1InputXYReturnZ。其中XYZ代表你测试的输入和输出,XYZ的值可以是泛化的,也可以是具体的,以测试的输入和输出来指定你的测试案例的名字来避免出现重复的测试案例。
一个测试文件下面可以包含很多个测试用例,我们可以把这个文件归类为一个用户故事。
我们也可以把多个用户故事归类为一个测试套件。
一个工程下面可以包含多个测试的套件。
通过以上的管理,我们可以让其他的开发者快速的了解我们已经创建的测试案例, 当然也可以避免单元测试的重复创建问题。
这里要说一下单元测试代码重复的问题。
一个很有意思的现象,比如对于针对同一个函数的测试案例,可能有很多相似的地方,那么有的人,就喜欢把这些相似的地方抽取出来,作为公共函数。
从我的经验来看,如果能够保证整个程序代码的复杂度不变是可以的。
首先测试代码部分保持重复,主要是为了保证整个逻辑的线性化。我们力求每个测试案例的复杂度为O(1)。杜绝这一点,除非非常有必要, 我们不想使用一些条件判断。
其次,如果公用代码可以放到测试初始化函数里面的话是最好的。这样同时也减少了测试案例内部的代码。
再次,虽然我们把测试代码当作产品代码来看,但是在实际的产品发布程序包中,我们是不会包含测试代码进去的。当然,这里有个例外,除非我们的产品本身就是开源的。即便如此,我们也不应该用实际产品代码的眼光去评判测试代码,因为测试代码的存在依据就是产品代码,也就是说测试代码并不是独立存在的。所以没有必要在测试代码中使用非常绚丽的编程技巧,平铺直叙,返璞归真才是正确的做法。
集成测试跟单元测试类似,区别在于它关心的是组件之间的测试。集成测试又称为功能集成测试。一般是基于业务需求而进行的针对跨组件的测试。
这部分的代码量一般来说要比单元测试要少。
子系统间测试是广泛意义上的功能集成测试,它针对的目标是子系统之间。一个比较常见的例子是通过调用多个微服务系统的接口,实现某项功能。再比如调用多个模块的接口,实现某项业务需求。
这部分的代码量一般来说比集成测试要少。
2. 微软如何判断测试代码质量?
我在微软做过几个项目。每个项目都要求有单元测试和集成测试。单元测试的测试覆盖率必须在85%以上才算过关,每次有修改的时候,所有的测试案例都要跑一遍,保证100%通过。
上面是工具上的一些指标。
在编写测试案例的过程中,不管是单元测试还是集成测试,测试框架都是离不了的。使用测试框架的好处就是我们只关心如何去添加测试案例,不需要关心太多其他的配置工作了。
深入到代码层级的质量主要是通过代码审查来控制的。代码审查的参考有这么几个因素:
测试套件的名字,测试案例的名字,测试案例的输入和输出,测试代码的复杂度,以及测试案例的有效性,这里的有效性主要是指测试路径的有效性。
详细点说,好的单元测试应该有如下的属性:
1. 自动化,结果的检查应该是自动化的,测试案例代码运行完成以后应该返回成功还是失败。
2. 可重复的,一个测试案例,你不管运行多少次,它的结果都应该是一样的。
3. 独立性高,一个测试案例不应该依赖于其他的测试案例。一个测试案例只应该集中测试一个事情。
4. 可读性强,测试的命名规则要统一,要像看待产品代码一样看待测试代码。
5. 运行速度要快,因为单元测试的执行频率比较高,如果速度比较慢的话,会影响开发效率。
3. 微软测试分层怎么做的?
首先微软是存在测试分层的,主要有如下几个层级: 单元测试,集成测试和端到端的自动化测试。
现在看一下这几种测试是如何分工的:
单元测试负责的是函数一级的测试,是基于函数的输入输出和返回值而创建的测试案例。
这种测试案例一般会选取一个正确执行的情况下的案例,再选几种错误输入导致失败的案例。如果函数相对来说比较复杂,也可能存在多种输入产生正确输出的情况。这个以具体函数的运行路径为参考标准。
集成测试又称为功能测试,是基于组件这一级的测试。一个组件包含多个函数,我们在测试组件的时候,一般我们是测试这个组件的公共接口。有的程序员也会使用一些技术来测试组件的私有接口,孰是孰非,这个要根据具体的场景来推断,一般来说我们不需要测试组件的私有函数。
这部分的测试案例一般会跟调用用户的功能需求相关的,比如说我们测试创建用户这个接口:
在输入参数都正确的情况下,正确的创建了用户让测试案例通过。
也可以输入错误的参数,无法创建用户,预期失败,从而让案例通过。
端到端的测试,一般是模拟用户操作的界面自动化测试。比如模拟鼠标点击来创建一个用户。创建用户可以有多种情况,一个是正确的创建了用户,其他情况如缺失了姓名的情况下创建用户或者缺失地址的情况下创建用户,或者没有使用有效的电话号码的情况下创建用户失败。
这都是端到端自动化测试的例子。
4. 开发者测试做不好的原因
首先说一下思想层面的问题,没有测试的代码是不完整的,那这里就有一个问题,为什么我们写完代码一定要写测试呢?
在回答这个问题之前,我们先问一下自己,我们写完这个代码是不是写完就以后不管了,如果不管了那就没必要写测试。
只要以后还想维护,那么测试代码是必不可少的,第1个原因,你的代码可能会被别人去接手,别人并不懂你的思路,那么测试案例就是别人理清你代码逻辑的重要依据,同时也是在别人修改你代码的时候的一个重要验证标准。第2个原因,即使代码是由你自己来维护的,之前写的代码,过段时间以后再去看的时候,你可能会感觉比较陌生了,因为遗忘是有规律的。
所以我们会看到让自己哭笑不得的一幕:
有的程序员骂看到的代码非常烂,结果查一下历史记录,是他自己两三年之前写的。
这种现象并不是孤立的,因为随着时间的推移,我们经验的增长,技术水平的不断提高,我们的眼光也会随之升高,再回头去看我们原先的代码的时候,就会发现那些代码比较弱智,这是程序员的人之常情。
在这种情况下,我们自然的想进行重构,调整以前的代码,如果以前有测试案例的话,我们会非常从容,继而省去不少时间的开销。
其次是技术层面的问题,因为测试代码跟我们实际的生产代码技术的侧重点是不一样的,测试代码这一部分我们需要做一些模拟数据,同时需要测试的框架。
测试框架的作用,一是帮助我们完成测试的自动化自动验证,如成功还是失败,同时也提供数据模拟的机制。
关于数据模拟部分,我们需要寻找测试替身,测试替身有很多种类:
其中最简单的就是Dummy, Dummy的模式最简单,我们可以把它理解成固定的数据提供模式,比如随便返回一个默认值。
另一个叫stub,它是在Dummy的基础上添加了简单的逻辑,提供了不同的输出,比如说根据输入的参数来返回不同的默认值。
再一个是spy,它可以捕获和监听测试对象的状态,以获得更高级的状态验证,比如可以用它来监听某个函数是否被调用了。
再一个是mock, mock的功能比上面的几个都要强大,它既能够提供定制的输入又可以进行监测。
最后一个是simulator即仿真器,仿真器相对来说比较复杂,它是一个全面的软件组件,它比上面几种方式都要接近真实的数据状态。但是仿真器如果没有现成的,相对来说开发难度比较大。
接下来说一下测试代码编写的时机。
第1个时机是在代码创建之初,或者在重构之前要先添加测试框架支持。这个时候的添加效果,我们希望可以有一个hello world的测试案例输出。
第2个时机是代码成型之后再进行测试案例代码的添加,如果代码还没有成型就去添加测试案例,因为代码的执行路径还没有确定,这会导致你不断的修改你的测试案例代码,不必要的工作量。当然这个地方有一个疑问点,就是如果你采用的是tdd (测试驱动开发), 那就另当别论,因为这种方法是要求先写测试案例, 再填充生产代码。
最后一个是测试代码的复杂度,这个看似容易理解,但很多人经常犯的一个错误。测试代码要追求平铺直叙,复杂度越低越好。除非万不得已,不要有任何的条件判断,比如像if else switch这种条件判断就免了,条件判断会增加你测试代码的复杂度,破坏测试单元的目标单一性。
5. 开发者测试覆盖率高低背后的意义
总的来说,在可以掌控的范围内,开发者测试的覆盖率当然是越高越好。
这里我们需要了解的是,覆盖率指的是什么。
我们平时看到的覆盖率的指标,比如说85%以上,100%以上这些都是代码行一级的覆盖率。比如100%的覆盖率呢,就是要求代码的每一行都要跑到。
我们举个例子,我们现在有这么一段函数代码,有两个if else分支是线性排列的。要达到100%的代码覆盖率,我们只需要创建两个测试案例,一个是让两个if同时成立,另一个是让两个else同时成立就可以了。
但是从分支上来说,上面的两个分支我们可以产生4种路径:
两个if同时成立,两个else同时成立,第1个if和第2个else成立,另一个else和另一个if成立。
从上面的分析中我们可以看出,为什么100%的代码覆盖率不能完全发现问题。
这主要是因为尽管代码行覆盖率已经达到了很高的指标,但是分支这一级的路径覆盖率还有遗漏。
小结
最后说一说开发者测试做到多少才够。我的观点如下:
开发者测试做还是不做,做到什么程度有一个底线,
1. 对项目组内部就是开发者是否能够自如的掌控自己所在的这个项目;
2. 对项目组外面要拿的出可靠的数据证明自己的项目是可靠的;
- 点赞
- 收藏
- 关注作者
评论(0)