优测云测试平台 | 有效的单元测试
一、前言
本文作者提出了一种评价单元测试用例的质量的思路,即判断用例是否达到测试的“四大目标”。掌握识别好的用例的能力,可以帮助我们高效地写出高质量的测试用例。
评判冰箱的好坏,并不需要有制造一台冰箱的能力。在开始写测试用例之前,可以先掌握识别好的用例的能力,这样可以避免我们自己花费大量的时间写出低质量的用例。要评价用例的质量好坏,就看测试是否达到我们期望的目标。
二、测试的第一目标是“尽可能地”排除缺陷
当我们给系统增加功能时,首先要保证增加的功能没有缺陷,同时还要防止回归。“回归”(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中的两个强大库,为我们实现自动化操作提供了便利。本文将向大家介绍如…...
LeetCode刷题 -- 542. 01矩阵 基于 DFS 更新优化的多源最短路径实现
LeetCode刷题 – 542. 01矩阵 基于 DFS 更新优化的多源最短路径实现 题目描述简述 给定一个 m x n 的二进制矩阵 mat,其中: 每个元素为 0 或 1返回一个同样大小的矩阵 ans,其中 ans[i][j] 表示 mat[i][j] 到最近 0 的最短曼哈顿距离 算法思…...
强化学习入门:交叉熵方法数学推导
前言 最近想开一个关于强化学习专栏,因为DeepSeek-R1很火,但本人对于LLM连门都没入。因此,只是记录一些类似的读书笔记,内容不深,大多数只是一些概念的东西,数学公式也不会太多,还望读者多多指教…...
Java+Access综合测评系统源码分享:含论文、开题报告、任务书全套资料
JAVAaccess综合测评系统毕业设计 一、系统概述 本系统采用Java Swing开发前端界面,结合Access数据库实现数据存储,专为教育机构打造的综合测评解决方案。系统包含学生管理、题库管理、在线测评、成绩分析四大核心模块,实现了测评流程的全自…...
java面试场景题:QPS 短链系统怎么设计
以下是对文章的润色版本: 这道场景设计题,初看似乎业务简单,实则覆盖的知识点极为丰富: 高并发与高性能分布式 ID 生成机制;Redis Bloom Filter——高并发、低内存损耗的过滤组件知识;分库、分表海量数据存…...

electron-vite串口通信
一、构建项目后,安装“串口通信库” npm install serialport二、设置 npm install --save-dev electron-rebuild ./node_modules/.bin/electron-rebuild 注意:如果执行报错以下问题 1、未配置python变量 2、没有Microsoft Visual Studio BuildTools 3…...
指针的定义与使用
1.指针的定义和使用 int point1(){//定义指针int a 10;//指针定义语法: 数据类型 * 指针变量名int * p;cout << "sizeof (int(*)) --> " << sizeof(p) << endl;//让指针记录变量a的地址 & 取址符p &a ;cout << &qu…...

图卷积网络:从理论到实践
图卷积网络(Graph Convolutional Networks, GCNs)彻底改变了基于图的机器学习领域,使得深度学习能够应用于非欧几里得结构,如社交网络、引文网络和分子结构。本文将解释GCN的直观理解、数学原理,并提供代码片段帮助您理…...
指针与函数参数传递详解 —— 值传递与地址传递的区别及应用
资料合集下载链接: https://pan.quark.cn/s/472bbdfcd014 在C语言中,函数参数的传递方式主要有两种:值传递和地址传递(通过指针)。理解两者的区别及应用对于正确操作数据和优化程序逻辑至关重要。本文将通过…...
C++动态规划-线性DP
这是一套C线性DP题目的答案。如果需要题目,请私信我,我将会更新题干 P1:单子序列最大和 #include <bits/stdc.h> using namespace std; int n,A,B,C; int a[200005]; int s[200005]; int main() {ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)…...

常用函数库之 - std::function
std::function 是 C11 引入的通用可调用对象包装器,用于存储、复制和调用任意符合特定函数签名的可调用对象(如函数、lambda、函数对象等)。以下是其核心要点及使用指南: 核心特性 类型擦除 可包装任意可调用对…...