优测云测试平台 | 有效的单元测试
一、前言
本文作者提出了一种评价单元测试用例的质量的思路,即判断用例是否达到测试的“四大目标”。掌握识别好的用例的能力,可以帮助我们高效地写出高质量的测试用例。
评判冰箱的好坏,并不需要有制造一台冰箱的能力。在开始写测试用例之前,可以先掌握识别好的用例的能力,这样可以避免我们自己花费大量的时间写出低质量的用例。要评价用例的质量好坏,就看测试是否达到我们期望的目标。
二、测试的第一目标是“尽可能地”排除缺陷
当我们给系统增加功能时,首先要保证增加的功能没有缺陷,同时还要防止回归。“回归”(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必填项校验方法 先看一下效果图(想在表单里动态的增删 form-item,然后添加rules,校验其必填项; ): html部分 <div v-for"(item, index) in …...

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

【每周一测】Java阶段二第四周学习
目录 1、request中的getParameter(String name)方法的功能是 2、request中的getParameter(String name)方法的功能是 3、spring创建bean对象没有以下哪个方式 4、spring依赖注入中没有以下哪个方式 5、RequestParam、RequestBody、PathVariable的应用场景及区别 6、Cooki…...

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

顺序表ArrayList
作者简介: zoro-1,目前大二,正在学习Java,数据结构等 作者主页: zoro-1的主页 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖 顺序表 概念Arraylist构造方法相关方法遍历操作 自…...
python软件安装技巧
安装软件时候加上源地址去安装,快速稳 pip install openni -ihttps://pypi.tuna.tsinghua.edu.cn/simple...

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

Vue虚拟节点和渲染函数
1.虚拟节点 虚拟节点(dom)本质上就是一个普通的JS对象,用于描述视图的界面结构 2.渲染函数render():接收一个 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日,周日上午 当在栈上创建一个对象时,计算机会为该对象分配一块连续的内存空间。该内存空间的位置在栈帧中,栈帧是用来存储函数调用信息和局部变量的一块内存区域。 栈帧中包含一个指针,称为栈指针(stack…...
iOS使用CoreML运用小型深度神经网络架构对图像进行解析
查找一个图片选择器 我用的是ImagePicker 项目有点老了,需要做一些改造,下面是新的仓库 platform :ios, 16.0use_frameworks!target learnings dosource https://github.com/CocoaPods/Specs.gitpod ImagePicker, :git > https://github.com/KevinS…...

使用Python打造微信高效自动化操作教程
引言 在如今数字化时代,人们对于效率的追求越来越强烈,尤其是在工作和学习中。自动化操作成为了提高生产力的有效途径之一,而PyAutoGUI和Pyperclip作为Python中的两个强大库,为我们实现自动化操作提供了便利。本文将向大家介绍如…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql
智慧工地管理云平台系统,智慧工地全套源码,java版智慧工地源码,支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求,提供“平台网络终端”的整体解决方案,提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...

visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...

SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题
分区配置 (ptab.json) img 属性介绍: img 属性指定分区存放的 image 名称,指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件,则以 proj_name:binary_name 格式指定文件名, proj_name 为工程 名&…...

AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...

基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...

uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
多模态图像修复系统:基于深度学习的图片修复实现
多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...