优测云测试平台 | 有效的单元测试

一、前言

本文作者提出了一种评价单元测试用例的质量的思路,即判断用例是否达到测试的“四大目标”。掌握识别好的用例的能力,可以帮助我们高效地写出高质量的测试用例。

评判冰箱的好坏,并不需要有制造一台冰箱的能力。在开始写测试用例之前,可以先掌握识别好的用例的能力,这样可以避免我们自己花费大量的时间写出低质量的用例。要评价用例的质量好坏,就看测试是否达到我们期望的目标。

二、测试的第一目标是“尽可能地”排除缺陷

当我们给系统增加功能时,首先要保证增加的功能没有缺陷,同时还要防止回归。“回归”(regression) 意指系统在增加了一些功能后,一些旧的功能出现缺陷。测试用例是否最大范围地去挖掘了系统的缺陷,最广为认知的手段就是计算测试覆盖率。但是关于覆盖率有一些认知需要澄清。
覆盖率高是不够的!
测试覆盖率低,就是系统的代码只有很少一部分被测试过了,那些未测试的部分是好是坏不知道。但是测试覆盖率高却并不意味着测试质量高,简单的例子就是无断言的测试用例,覆盖率可以很高,但是它跟没有测试几乎是一样的。不过还有更违反直觉的事实可以看一下一个简单的例子:
listing 1

func IsStringLong(s string) bool {if len(s) > 5 {return true}return false}func TestIsStringLong(t *testing.T) {got := IsStringLong("abc")assert.Equal(t, false, got)}

被测代码一共 6 行,测试覆盖到了 1,2,5 行,覆盖率 50%. 然后我们测试代码不变,被测代码简化一下
listing 2

func IsStringLong(s string) bool {return len(s) > 5 }

马上覆盖率就达到了 100%. 很显然这个 100% 的覆盖率并不充分,它都没有测试 s > 5 的情况。了解测试的同学马上会想到,上面覆盖率的概念其实是覆盖率的一种,叫行覆盖率(其实英文的 statement coverage 会更加确切)。另外一种覆盖率叫做分支覆盖率,IsStringLong 有 2 个逻辑分支,我们的测试代码只覆盖了其中一个,为了充分测试,我们要提供分支的覆盖率。
listing3

func TestIsStringLong(t *testing.T) {type args struct {s string}tests := []struct {name stringargs argswant bool}{{name: "'abcde' results short",args: args{s: "abcde",},want: false,},{name: "'abcdef' results long",args: args{s: "abcdef",},want: true,},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {if got := IsStringLong(tt.args.s); got != tt.want {t.Errorf("IsStringLong() = %v, want %v", got, tt.want)}})}
}

撒花,我们测试了所有分支!但是全部的分支覆盖率也存在问题,我们来看另一个例子:

listing 4

type Recorder struct {Value string
}var recorder = Recorder{}func IsStrLong(s string) bool {recorder.Value = sreturn len(s) > 5
}func TestIsStrLong(t *testing.T) {type args struct {s string}tests := []struct {name stringargs argswant bool}{{name: "'abcde' results short",args: args{s: "abcde",},want: false,},{name: "'abcdef' results long",args: args{s: "abcdef",},want: true,},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {if got := IsStrLong(tt.args.s); got != tt.want {t.Errorf("IsStrLong() = %v, want %v", got, tt.want)}})}
}

被测函数增加了一项功能,记录最后一次调用的参数。测试代码不变,同样还是 100% 的行覆盖和 100% 的分支覆盖,但是 “recoreder 里是否有正确记录最后一次参数” 却无法得到保障。假设这段代码提交以后,下个迭代某个开发失手删除了:

recorder.Value = s

这一行,测试流水线依然通过,甚至因为全覆盖还会给你发个点赞的信息。但是项目上线后却可能因为这段记录丢了引发功能故障。解决上面的问题就是在断言阶段,增加对 recorder.Value 的断言:
listing 5

for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {if got := IsStrLong(tt.args.s); got != tt.want {t.Errorf("IsStrLong() = %v, want %v", got, tt.want)}if recorder.Value != tt.args.s {t.Errorf("IsStrLong() called but recorder.Value = %v, want %v", recorder.Value, tt.args.s)}})

变异测试可以辅助评价断言质量,但是性能开销巨大
接着上面的例子,listing 4 中我们看到一类断言不足,但是覆盖率 100% 的用例,这种情况我们可以用变异测试来检测。其大致原理是流水线在启动后,随机修改被测代码,而测试代码不变,然后运行测试,若用例依然能通过,则预示着测试用例可能质量不高。以 listing 4 为例,变异测试引擎生成的一个版本是删除了:

recorder.Value = s

这一行代码,对变异版本运行用例会发现用例通过,则该用例的变异得分会低。而相同的变异版本, listing 5 中的用例会失败。

变异测试会对被测代码的语法树作各种变异,要对每个变异版本进行测试,其工作量是巨大的,耗时可想而知。因此对整个代码库进行变异测试,通常不适合放在对耗时要求较高的 CR 流水线上。可行的方法有:

-1.在定时流水线上对被测系统全量运行,被测系统比较大时,可以分模块进行。

-2.如果被测系统组织良好,CR 流水线进准测试(分片测试)能力足够高,则可以在 CR 流水线上对改动部分作变异测试。

实践建议

  • 1.每次 CR 统计覆盖率,特别是增量覆盖率, 覆盖率过低时阻挡合入。[1]

  • 2.CR 的 reviewer 需注意断言是否充分合理,相对变异测试,负责任的高水平的 reviewer 效率更高。因此 CR 单的 change list 应该尽可能小,这样 CR 通过才能尽可能快。

  • 3.引入定时流水线,分模块对代码库进行变异测试。这需要根据实际性能调整策略。

代码覆盖率高是不够的!终极的覆盖

以 tRPC-Go 数据校验为例。tRPC-Go 有配套的数据校验工具,其原理简单说就是在 proto 文件中增加 Message 各字段的校验规则,在 tRPC 服务中引入校验拦截器,在运行时拦截器会针对入参进行校验。
step 1 在 proto 中增加校验规则


// QueryCaseRecentExecsRequest 包含用例 id, 用来查询其最近 n 次执行记录, 最多查询最近 100 次记录。
message QueryCaseRecentExecsRequest {sint64 case_id = 1;uint32 count = 2[(validate.rules).uint32.lte = 100];
}

step 2 在服务的 trpc_go.yaml 中配置使用拦截器


server:filter:- validation
}

step 3 在服务的 main.go 中注册拦截器


import (// ..._ "git.code.oa.com/trpc-go/trpc-filter/validation"
)func main() {// ...
}

在服务的方法中,就不需要再做这类校验了


func (s *XXXService) QueryCaseRecentExecs(ctx context.Context, req *proto.QueryCaseRecentExecsRequest, rsp *proto.QueryCaseRecentExecsReply) error {// 不再需要//if req.Count > 100 {//  return errors.New("count not allowed")//}result, err := s.CaseExecService.QueryRecentExecsByCaseID(ctx, req.CaseId, int(req.Count))if err != nil {return err}rsp.CaseExecs = pbconv.FromCaseExecs(result)return nil
}

在这样的代码库中,不管是 XXXService 还是 XXXService.CaseExecService (domain service) 都不需要对 count 进行拦截校验了。即使单测代码全覆盖,我们也无法保证我们 step 1,2,3 都按照文档正确地配置了,更进一步,即使我们非常仔细地检查了配置,也不能保证规则检查正确地生效了,毕竟,谁知道

git.code.oa.com/trpc-go/trpc-filter/validation

有没有 bug?这个问题的最终解决方案就是将服务部署起来,向它发请求,来确保参数校验确确实实生效了(接口测试或者端到端测试)。可能有的同学会有疑问“不要测试框架代码”这样的建议有错吗?建议没错,但那是针对单元测试的。但是我们的自己的产品在发布前,不管是哪种原因产生的缺陷,都应该尽量通过测试来排查出来,框架不行就替换框架。产品出问题,用户才不关心是开发者造成的还是框架造成的。请注意完整测试我们的系统并不是建议大家完整地测试我们用到的每一个框架每一个库,而是测试我们系统的每个功能。[2]

三、测试的目标之二:支撑重构

理想的测试用例应该只检验被测系统的输入输出(输出包含通常意义的返回值和一切副作用,如上文提到的状态改变),而不应该关心系统到底是怎么实现这个功能的。这样当我们重构一段代码时,我们只要针对修改后的功能代码运行原有的测试用例,当用例通过时就证明我们的重构没有引入缺陷。如果你重构一段代码后发现原有的用例无法通过了,但是我们自己对重构前后的功能一致非常有信心,此时不得不“微调”一下用例来保证用例通过,每当此时就应该意识到这些用例在支撑重构上做得不够好。功能不变的情况下,通过改变实现而让测试用例失败的情况称之为误警(false alarm),或者叫假阳性(false positive). 熟悉 SRE 的同学对误警应该不会陌生。假阳性最好的类比就是医学上假阳性:没病检验出病。事实上自动化测试跟医学检验非常相似,医学检验的英文也叫 test,有假阳性,也有假阴性,而且甚至都没有办法完全排除假阳性和假阴性的发生。

支撑重构做得不好的危害:

  • 1.就假阳性而言,假阳性如果多了,会麻痹大家对测试失败的认知,导致团队成员忽略一些失败用例。这是也是 flakey test 的问题。

  • 2.每次重构都要增加维护测试的开销,这违背了自动化测试的初衷。内网上已经有开发在讨论做测试带来的“额外”负担该由谁承担之类的问题,过大的负担会阻碍团队成员对自动化测试的接纳。当开发者把写用例当作一种不情愿的任务来完成时,自动化测试的质量想必不会太好。

  • 3.支撑重构做得不好往往意味着用例检验了功能以外的东西,这其实在某种意义也是一种断言不当,它反映了用例作者对测试的误解,暗示着用例还可能有其他的问题。
    支撑重构做得不好的重要信号是过度依赖 mock,特别是那些在 mock 规则中还增加了调用顺序校验的。这也是为什么在各个场合中,一些测试布道者偏爱基于输入输出的测试(classical testing),而不是基于 mock 的测试(mockist testing)。但是有时候,团队流水线对覆盖率有硬性质量红线,使用 classical testing 无法达到那么高的覆盖率,我们不得不做一些 mock 来通过质量红线。因此应该尽量避免设置过高的质量红线,而应该培养团队成员的 testing sense, 程序员的价值就在这里。

先更新到这里,下篇继续~
优测测试平台简介:
是一个为企业与开发者提供专业的测试工具和服务的平台,沉淀十年产品测试经验,提供终端测试、接口测试、性能测试、安全测试等多领域测试服务与产品,协助客户提高效率降低成本,保证产品质量。
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/167369.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

经典链表问题:解析链表中的关键挑战

这里写目录标题 公共子节点采用集合或者哈希采用栈拼接两个字符串差和双指针 旋转链表 公共子节点 例如这样一道题:给定两个链表,找出它们的第一个公共节点。 具体的题目描述我们来看看牛客的一道题: 这里我们有四种解决办法: …

晶振与晶体

文章目录 基础知识无源晶振 & 有源晶振 博文链接 基础知识 无源晶振 & 有源晶振 博文链接 晶振原理解析

Flutter的Constructors for public widgets should have a named ‘key‘ parameter警告

文章目录 问题描述问题原因修改方法详细解释 问题描述 Constructors for public widgets should have a named ‘key’ parameter. 如下图: 原本的代码 class MyTabPage extends StatefulWidget {override_MyTabPageState createState() > _MyTabPageState(…

大数据测试用例分析

基于大数据分析,对业务系统产生的日志进行智能分析,能够识别日志中的接口、参数、业务流,并依据分析的结果生成测试用例。 问题与背景 业务复杂 业务系统的复杂性,对测试人员的业务能力提出严格要求,加重测试成本。 …

【深度学习-第4篇】使用MATLAB快速实现CNN多变量回归预测

上一篇我们讲了使用CNN进行分类的MATLAB代码。 这一篇我们讲CNN的多变量回归预测。 是的,同样是傻瓜式的快速实现。 一、什么是多变量回归预测 多变量回归预测则是指同时考虑多个输入特征进行回归预测。举几个例子: 房价预测:给定一组房…

搜索问答技术学习:基于知识图谱+基于搜索和机器阅读理解(MRC)

目录 一、问答系统应用分析 二、搜索问答技术与系统 (一)需求和信息分析 问答需求类型 多样的数据源 文本组织形态 (二)主要问答技术介绍 发展和成熟度分析 重点问答技术基础:KBQA和DeepQA KBQA(…

CentOS 系统安装和使用Docker服务

系统环境 使用下面的命令,可以查看CentOS系统的版本。 lsb_release -a结果: 说明我的系统是7.9.2009版本的 安装Docker服务 依次执行下面的指令: yum install -y yum-utilsyum install -y docker即可安装docker服务 如果这样安装不成功…

[ Windows-Nginx ]Windows服务器,Tomcat容器部署项目,整合Nginx

一、官网下载Nginx http://nginx.org/en/download.html 稳定版:windows的stable版本 注意:Nginx安装包不要放在中文目录下 二、conf目录下,修改nginx.conf文件 修改Nginx服务端口: 默认端口为80,即外界访问的入口…

mysql优化之explain详解

mysql的explain(执行计划)用于解释sql的执行的过程,然后把sql的执行过程用一张表格表示出来,它并不真正的执行sql,如下图。explain能够为我们优化sql提供很好参考作用。 下面我来看下执行计划表中各个字段是什么意思 i…

FFmpeg和rtsp服务器搭建视频直播流服务

下面使用的是ubuntu的,window系统可以参考: 通过rtsp-simple-server和ffmpeg实现录屏并发布视频直播_rtsp simple server_病毒宇宇的博客-CSDN博客 一、安装rtsp-simple-server (1)下载rtsp-simple-server 下载地址:R…

第 368 场 LeetCode 周赛题解

A 元素和最小的山形三元组 I 前后缀操作&#xff1a;求出前后缀上的最小值数组&#xff0c;然后枚举 j j j class Solution { public:int minimumSum(vector<int> &nums) {int n nums.size();vector<int> l(n), r(n);//l[i]min{nums[0],...,nums[i]}, r[i]mi…

二维码智慧门牌管理系统升级解决方案:突破传统,实现质检与抽检的个性化配置

文章目录 前言一、引入“独立质检”二、个性化抽检类别设定三、触发重采要素的功能升级四、升级优势与展望 前言 在数字化时代&#xff0c;智慧门牌管理系统已经成为社会管理的重要工具。为了满足各种复杂需求&#xff0c;系统升级是必然趋势。本次升级主要针对质检和抽检两大…

【试题038】 逻辑与和赋值表达式例题

1.题目&#xff1a;设int n;&#xff0c;执行表达式(n2)&&(n1)&&(n0)后&#xff0c;n的值是&#xff1f; 2.代码分析&#xff1a; //设int n;&#xff0c;执行表达式(n2)&&(n1)&&(n0)后&#xff0c;n的值是? int main() {int n;printf("…

Java高级编程----集合

集合 集合概述Collection接口List接口简介ArrayList集合Set接口简介Hash Set接口简介Map接口简介TreeMap集合Properties集合 集合概述 为了在程序中可以保存数目不确定的对象&#xff0c;Java提供了一系列特殊类&#xff0c;这些类可以存储任意类型的对象&#xff0c;并且长度…

在Espressif-IDE中使用Wokwi仿真ESP32

陈拓 2023/10/17-2023/10/19 1. 概述 在Espressif-IDE v2.9.0版本之后可直接在IDE中使用Wokwi模拟器。 1.1 什么是 Wokwi 模拟器&#xff1f; Wokwi 是一款在线电子模拟器&#xff0c;支持模拟各种开发板、元器件和传感器&#xff0c;例如乐鑫产品 ESP32。 Wokwi 提供基于浏…

推理引擎之模型压缩浅析

目录 前言1. 模型压缩架构和流程介绍2. 低比特量化原理2.1 量化基础介绍2.2 量化方法2.3 量化算法原理2.4 讨论 3. 感知量化训练QAT原理3.1 QAT原理3.2 量化算子插入3.3 QAT训练流程3.4 QAT衍生研究3.5 讨论 4. 训练后量化PTQ4.1 动态PTQ4.2 静态PTQ4.3 KL散度实现静态PTQ4.4 量…

SystemVerilog Assertions应用指南 Chapter 1.14蕴含操作符

1.14蕴含操作符 属性p7有下列特别之处 (1)属性在每一个时钟上升沿寻找序列的有效开始。在这种情况下,它在每个时钟上升沿检查信号“a”是否为高。 (2)如果信号“a”在给定的任何时钟上升沿不为高,检验器将产生一个错误信息。这并不是一个有效的错误信息,因为我…

Leetcode 454 四数相加II(哈希表 + getOrDefault方法用于获取Map中指定键的值,如果键不存在,则返回一个默认值)

Leetcode 454 四数相加II&#xff08;哈希表&#xff09; 解法1 HashMap getOrDefault方法 解法1 HashMap getOrDefault方法 【HashMap】 【⭐️HashMap常用操作】 创建HashMap&#xff1a;HashMap<Integer, Integer> hash new HashMap<>(); 向HashMap添加元素…

【类和对象+this引用】

文章目录 面向对象与面向过程面向对象关注的是对象&#xff0c;用类描述这个对象如何定义类如何更改类名 类的实例化this引用总结 面向对象与面向过程 面向对象就是解决问题的一种思想&#xff0c;主要依靠对象之间的交互完成一件事情。 面向过程好比传统的洗衣服方式&#x…

17 Transformer 的解码器(Decoders)——我要生成一个又一个单词

Transformer 编码器 编码器在干吗&#xff1a;词向量、图片向量&#xff0c;总而言之&#xff0c;编码器就是让计算机能够更合理地&#xff08;不确定性的&#xff09;认识人类世界客观存在的一些东西 Transformer 解码器 解码器会接收编码器生成的词向量&#xff0c;然后通…