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

一、前言

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

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

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

当我们给系统增加功能时,首先要保证增加的功能没有缺陷,同时还要防止回归。“回归”(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.mzph.cn/news/115554.shtml

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

相关文章

【unity3D】Scroll Rect组件—制作下滑列表

💗 未来的游戏开发程序媛,现在的努力学习菜鸡 💦本专栏是我关于游戏开发的学习笔记 🈶本篇是unity的Scroll Rect组件 Scroll Rect组件 基础知识详细说明案例演示——制作一个简单的下滑框扩展 介绍:Scroll Rect组件是用…

装备控制类嵌入式软件开发知识

1.嵌入式系统概念及特点 嵌入式系统是一种可深埋在某一专用设备中的系统。一般而言,由于设备的体积、重量和功耗的限制,嵌入式系统将受到多种条件和环境的制约,这也对嵌入式系统提出了有别于其他计算机系统的特殊要求。 通常,嵌…

【万字长文】向 AI 提问的艺术

向 AI 提问的艺术 本文是我在学习 Prompt Engineering 过程中,总结出来的一些经验和方法。里边包含一些自己的心得和验证有效的技巧。这些技巧在很多其他文章中也有介绍,这里可以当作是一个集大成的汇总。 我会按照“道——法——术”三个层面来介绍向 …

【最经典的79个】软件测试面试题(内含答案)

001.软件的生命周期(prdctrm) 计划阶段(planning)-〉需求分析(requirement)-〉设计阶段(design)-〉编码(coding)->测试(testing)->运行与维护(running maintrnacne) 测试用例 用例编号 测试项目 测试标题 重要级别 预置条件 输入数据 执行步骤 预期结果 0002.问&…

vue2中,下拉框多选和全选的实现

vue2中&#xff0c;下拉框多选和全选的实现 代码布局在methods: 中添加功能函数较为完整的一个整体代码&#xff1a; 如图所示点击全选即可完成下拉框中全部子项的全部的选中&#xff0c;同时取消全选即可全部取消选择。 代码布局 <div class"chos-box2"><…

如何给照片添加水印?请看下面3个简单教程

如何给照片添加水印&#xff1f;随着智能手机的普及和不断提升的拍摄技术&#xff0c;如今人们可以轻松使用手机进行高质量的照片拍摄。从老人到小孩&#xff0c;每个人都可以在日常生活中捕捉到美好瞬间&#xff0c;并将其记录下来。作为一种表达自己的方式&#xff0c;现在手…

安装pythonQt报错

在使用命令行pip install PyQt5安装pythonQt5的时候报错。 镜像源有问题&#xff0c;手动设置可用的源 pip install PyQt5 -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com tool工具 pip install PyQt5-tools -i https://pypi.douban.com/simple 安装成…

以赛促教,以赛促研 ——计算机科学系举办“火焰杯”软件测试开发选拔赛颁奖仪式

颁奖仪式 2023年3月9日&#xff0c;第三届“火焰杯”软件测试开发选拔赛颁奖仪式在南海楼124会议室举行&#xff0c;计算机科学系系主任龙锦益教授、指导老师孙玉霞副教授、测吧科技有限公司王雪冬总监及获奖同学参加了颁奖仪式。 会议伊始&#xff0c;龙锦益教授对王雪冬总监…

2023年9月青少年软件编程(Python) 等级考试试卷(一级)

青少年软件编程&#xff08;Python&#xff09;等级考试试卷&#xff08;一级&#xff09;202309 2023年9月 分数&#xff1a;100 题数&#xff1a;37 一、单选题(共25题&#xff0c;共50分) 1. 下列 Python 语句能够正确输出"学而时习之"五个字的是&#xff1f;…

C++ 纠错题总结2

1、for循环中的判断语句: 要注意初始赋值、< 还是 < for(int i 0; i < n; i) 2、cin.getline(char[], int, char) 注意&#xff1a;第二个参数为不是char[]的有效元素个数&#xff0c;因为最后一个元素位置要用来存储 \0 3、函数形参有默认值的&#xff0c;有默认…

使用python_opencv比较图像差异/使用python_opencv找出两张图像的差异范围

目录 1 创建conda环境 2 安装python库 2.1 报错 ModuleNotFoundError: No module named numpy 3 image_diff.py

Jupyter Notebook 设置黑色背景主题

Jupyter Notebook 设置黑色背景主题 # 包安装 pip install jupyterthemes -i https://mirrors.aliyun.com/pypi/simple pip install --upgrade jupyterthemes # 查看可用主题 jt -l # monokai暗背景&#xff0c;-f(字体) -fs(字体大小) -cellw(占屏比或宽度) -ofs(输出段的字…

labelimg使用以及xml和txt转化

一、使用 winr——输入cmd 激活已有的环境 conda activate torch1.2.0 下载lebelme pip install labelme3.16.7 再输入labelme打开软件就可 可以设置自动保存&#xff0c;View——auto save mode打上勾 二、注意 1.自己类的定义名称&#xff0c;在txt中是0&#xff0c;1&a…

C++ string 类相关知识

/* ISO/ANSI C标准通过添加string类扩展了C库,因此现在可以string类型的变量(使用C的话 说是对象)而不是字符数组来存储字符串。读者将看到,string 类使用起来比数组简单,同时提供了将字 符串作为一种数据类型的表示方法。 要使用string类,必须在程序中包含头文件string。s…

java: 警告: 源发行版 8 需要目标发行版 1.8;(可我的项目是其他版本啊?)

我的项目是1.7 可运行时却要求是1.8 情况1 提示需要1.8 而你刚好项目就是1.8 那么就是配置问题 这里就不描述怎么解决了 方案很多。 情况2 项目用的不是XX版本却提示需要XX版本 在你的Maven目录的conf文件夹下settings.xml中下面 不知道maven目录在哪从这里看 如果这里指…

H指数----题解报告

题目&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 由题目可知&#xff0c;我们要寻找的是一个最大的H值。 首先先对引用次数排序&#xff0c;然后再寻找在规定内的最大H值 public int hIndex(int[] citations) {Arrays.sort(citatio…

索引有哪些缺点以及具体有哪些索引类型

索引的优缺点 优点&#xff1a; 合理的增加索引&#xff0c;可以提高数据查询的效率&#xff0c;减少查询时间 有一些特殊的索引&#xff0c;可以保证数据的完整性&#xff0c;比如唯一索引 缺点&#xff1a; 创建索引和维护索引需要消耗时间 索引需要额外占用物理空间 对创建…

git初学者使用教程(包含Android studio中git使用)

文章目录 1、登录 / 注册git账号2、创建git仓库3、设置git的用户名和邮箱4、创建git仓库5、已有仓库推送代码到远程仓库6、进阶使用&#xff0c;命令行和Android studio软件使用1. 克隆&#xff08;Clone&#xff09;源码仓库2. 查看仓库状态&#xff08;Status&#xff09;3. …

fastadmin 后台添加视频

做个记录,字段自行对照解决 1.add.html <div class"form-group"><label class"control-label col-xs-12 col-sm-2">{:__(Video)}:</label><div class"col-xs-12 col-sm-8"><div class"input-group">&l…

【已解决】Word文档的“打开密码”在哪里设置?

我们知道&#xff0c;Word文档可以设置“打开密码”&#xff0c;保护文件不被随意打开&#xff0c;只有输入正确的密码才能打开文档。那这个“打开密码”要在哪里设置呢&#xff1f;不清楚的小伙伴往下看就知道啦。 在Word文档里&#xff0c;有两个路径可以设置“打开密码”。…