当前位置: 首页 > news >正文

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

一、前言

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

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

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

当我们给系统增加功能时,首先要保证增加的功能没有缺陷,同时还要防止回归。“回归”(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, 程序员的价值就在这里。

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

相关文章:

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

一、前言 本文作者提出了一种评价单元测试用例的质量的思路,即判断用例是否达到测试的“四大目标”。掌握识别好的用例的能力,可以帮助我们高效地写出高质量的测试用例。 评判冰箱的好坏,并不需要有制造一台冰箱的能力。在开始写测试用例之…...

Java设计模式之外观模式

定义 又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,…...

MyBatis实现延时加载的方式

MyBatis实现延时加载的方式有两种: 使用resultMap的association和collection标签配置延时加载:在查询语句中,使用association标签配置一对一关联关系,使用collection标签配置一对多关联关系。然后在查询结果映射的resultMap中配置…...

计算未来:微软眼中的人工智能

计算未来 :人工智能及其社会角色(The Future Computed. Artificial Intelligence and its role in society )这本书于2018年09月由北京大学出版社出版。 书籍的作者是:沈向洋(微软全球执行副总裁),(美&…...

字号和磅的对应关系

字号「八号」对应磅值5 字号「七号」对应磅值5.5 字号「小六」对应磅值6.5 字号「六号」对应磅值7.5 字号「小五」对应磅值9 字号「五号」对应磅值10.5 字号「小四」对应磅值12 字号「四号」对应磅值14 字号「小三」对应磅值15 字号「三号」对应磅值16 字号「小二」对应磅值18 …...

Bag of Tricks for Efficient Text Classification(FastText)

主要的有点就是快,用途就是用于文本分类,模型结构如上,主要是通过embedding将文本转换成向量,然后进行mean-pooling,然后输入到hidden隐向量中,通过softmax输出多分类,损失函数是对数似然损失函…...

vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法

vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法 先看一下效果图&#xff08;想在表单里动态的增删 form-item&#xff0c;然后添加rules&#xff0c;校验其必填项&#xff1b; &#xff09;: html部分 <div v-for"(item, index) in …...

使用 ClickHouse 深入了解 Apache Parquet (一)

​ 【squids.cn】 全网zui低价RDS&#xff0c;免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等 自2013年作为Hadoop的列存储发布以来&#xff0c;Parquet几乎已经成为一种无处不在的文件交换格式&#xff0c;它提供了高效的存储和检索。这种采纳使其成为更近期的…...

【每周一测】Java阶段二第四周学习

目录 1、request中的getParameter(String name)方法的功能是 2、request中的getParameter(String name)方法的功能是 3、spring创建bean对象没有以下哪个方式 4、spring依赖注入中没有以下哪个方式 5、RequestParam、RequestBody、PathVariable的应用场景及区别 6、Cooki…...

系统设计 - 我们如何通俗的理解那些技术的运行原理 - 第四部分:微服务架构

本心、输入输出、结果 文章目录 系统设计 - 我们如何通俗的理解那些技术的运行原理 - 第四部分&#xff1a;微服务架构前言典型的微服务架构是什么样的微服务的优势 微服务最佳实践在开发微服务时&#xff0c;我们需要遵循以下最佳实践&#xff1a; 微服务通常使用什么技术堆栈…...

顺序表ArrayList

作者简介&#xff1a; zoro-1&#xff0c;目前大二&#xff0c;正在学习Java&#xff0c;数据结构等 作者主页&#xff1a; zoro-1的主页 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f496; 顺序表 概念Arraylist构造方法相关方法遍历操作 自…...

python软件安装技巧

安装软件时候加上源地址去安装&#xff0c;快速稳 pip install openni -ihttps://pypi.tuna.tsinghua.edu.cn/simple...

解析Apache Kafka中的事务机制

这篇博客文章并不是关于使用事务细节的教程&#xff0c;我们也不会深入讨论设计细节。相反&#xff0c;我们将在适当的地方链接到JavaDocs或设计文档&#xff0c;以供希望深入研究的读者使用。 为什么交易? 我们在Kafka中设计的事务主要用于那些显示“读-进程-写”模式的应用…...

Vue虚拟节点和渲染函数

1.虚拟节点 虚拟节点&#xff08;dom&#xff09;本质上就是一个普通的JS对象&#xff0c;用于描述视图的界面结构 2.渲染函数render()&#xff1a;接收一个 createElement()函数创建的VNode Vue.component("board", {render: function(createElement) {return cr…...

后台交互-首页->与后台数据进行交互,wsx的使用

与后台数据进行交互wsx的使用 1.与后台数据进行交互 // index.js // 获取应用实例 const app getApp() const apirequire("../../config/app.js") const utilrequire("../../utils/util.js") Page({data: {imgSrcs:[{"img": "https://cd…...

【微服务保护】Sentinel 流控规则 —— 深入探索 Sentinel 的流控模式、流控效果以及对热点参数进行限流

文章目录 前言一、快速掌握 Sentinel 的使用1.1 什么是簇点链路1.2 Sentinel 的简单使用示例 二、Sentinel 流控模式2.1 直接模式2.2 关联模式2.3 链路模式 三、流控效果3.1 快速失败3.2 预热模式3.3 排队等待 四、对热点参数的流控4.1 热点规则4.2 热点规则演示 前言 微服务架…...

ZXing.Net 的Core平台生成二维码

一、引用 二、代码 帮助类 /// <summary>/// ZXing.NET 二维码帮助类/// </summary>public class ZXingHelper{/// <summary>/// 站点二维码的目录/// </summary>private static string QRCodeDirectory "QRCode";/// <summary>/// 使…...

【C++】假设给类分配的是栈的空间,那么计算机是如何访问栈中不同位置的对象的数据的呢?

2023年10月22日&#xff0c;周日上午 当在栈上创建一个对象时&#xff0c;计算机会为该对象分配一块连续的内存空间。该内存空间的位置在栈帧中&#xff0c;栈帧是用来存储函数调用信息和局部变量的一块内存区域。 栈帧中包含一个指针&#xff0c;称为栈指针&#xff08;stack…...

iOS使用CoreML运用小型深度神经网络架构对图像进行解析

查找一个图片选择器 我用的是ImagePicker 项目有点老了&#xff0c;需要做一些改造&#xff0c;下面是新的仓库 platform :ios, 16.0use_frameworks!target learnings dosource https://github.com/CocoaPods/Specs.gitpod ImagePicker, :git > https://github.com/KevinS…...

使用Python打造微信高效自动化操作教程

引言 在如今数字化时代&#xff0c;人们对于效率的追求越来越强烈&#xff0c;尤其是在工作和学习中。自动化操作成为了提高生产力的有效途径之一&#xff0c;而PyAutoGUI和Pyperclip作为Python中的两个强大库&#xff0c;为我们实现自动化操作提供了便利。本文将向大家介绍如…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合

强化学习&#xff08;Reinforcement Learning, RL&#xff09;是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程&#xff0c;然后使用强化学习的Actor-Critic机制&#xff08;中文译作“知行互动”机制&#xff09;&#xff0c;逐步迭代求解…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器

第一章 引言&#xff1a;语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域&#xff0c;文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量&#xff0c;支撑着搜索引擎、推荐系统、…...

uniapp微信小程序视频实时流+pc端预览方案

方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度​WebSocket图片帧​定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐​RTMP推流​TRTC/即构SDK推流❌ 付费方案 &#xff08;部分有免费额度&#x…...

jmeter聚合报告中参数详解

sample、average、min、max、90%line、95%line,99%line、Error错误率、吞吐量Thoughput、KB/sec每秒传输的数据量 sample&#xff08;样本数&#xff09; 表示测试中发送的请求数量&#xff0c;即测试执行了多少次请求。 单位&#xff0c;以个或者次数表示。 示例&#xff1a;…...

2025.6.9总结(利与弊)

凡事都有两面性。在大厂上班也不例外。今天找开发定位问题&#xff0c;从一个接口人不断溯源到另一个 接口人。有时候&#xff0c;不知道是谁的责任填。将工作内容分的很细&#xff0c;每个人负责其中的一小块。我清楚的意识到&#xff0c;自己就是个可以随时替换的螺丝钉&…...

李沐--动手学深度学习--GRU

1.GRU从零开始实现 #9.1.2GRU从零开始实现 import torch from torch import nn from d2l import torch as d2l#首先读取 8.5节中使用的时间机器数据集 batch_size,num_steps 32,35 train_iter,vocab d2l.load_data_time_machine(batch_size,num_steps) #初始化模型参数 def …...

Axure零基础跟我学:展开与收回

亲爱的小伙伴,如有帮助请订阅专栏!跟着老师每课一练,系统学习Axure交互设计课程! Axure产品经理精品视频课https://edu.csdn.net/course/detail/40420 课程主题:Axure菜单展开与收回 课程视频:...

无需布线的革命:电力载波技术赋能楼宇自控系统-亚川科技

无需布线的革命&#xff1a;电力载波技术赋能楼宇自控系统 在楼宇自动化领域&#xff0c;传统控制系统依赖复杂的专用通信线路&#xff0c;不仅施工成本高昂&#xff0c;后期维护和扩展也极为不便。电力载波技术&#xff08;PLC&#xff09;的突破性应用&#xff0c;彻底改变了…...

安宝特案例丨寻医不再长途跋涉?Vuzix再次以AR技术智能驱动远程医疗

加拿大领先科技公司TeleVU基于Vuzix智能眼镜打造远程医疗生态系统&#xff0c;彻底革新患者护理模式。 安宝特合作伙伴TeleVU成立30余年&#xff0c;沉淀医疗技术、计算机科学与人工智能经验&#xff0c;聚焦医疗保健领域&#xff0c;提供AR、AI、IoT解决方案。 该方案使医疗…...