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

news2024/11/7 20:38:52

一、前言

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

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

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

当我们给系统增加功能时,首先要保证增加的功能没有缺陷,同时还要防止回归。“回归”(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 string
    args args
    want 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 = s
  return len(s) > 5
}

func TestIsStrLong(t *testing.T) {
  type args struct {
    s string
  }
  tests := []struct {
    name string
    args args
    want 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.coloradmin.cn/o/1114778.html

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

相关文章

TikTok Shop印尼站关停,跨境卖家该如何自救

10.4日,TikTok Shop宣布关停印尼站,无疑是一个巨大的炸弹投在整个跨境圈,让许多跨境卖家以及当地本土的卖家、品牌措手不及。 这对TikTok的东南亚市场而言,无疑是一次沉重的打击。 TikTok在东南亚的商业布局,印尼是其…

小A对我说,他现在快想钱想疯了…

昨天晚上11点,小A给我打电话 小A问:橙哥,有没有赚钱的事做? 他说,实在不想上班了,每天起早贪黑挤地铁 辛辛苦苦干一个月,到手工资三四千块钱,房租一交,日常开支一花&a…

Jmeter安装(快速入门)

大家好我是苏麟今天简单聊一下Jmeter . Jmeter Jmeter官网 : Apache JMeter - Download Apache JMeter 安装Jmeter Jmeter依赖于JDK,所以必须确保当前计算机上已经安装了JDK,并且配置了环境变量。 下载 : 官网下载 : 网盘自取 : 链接:…

【LeetCode】57. 插入区间

1 问题 给你一个 无重叠的 ,按照区间起始端点排序的区间列表。 在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。 示例 1: 输入:interval…

国产开发板上打造开源ThingsBoard工业网关--基于米尔芯驰MYD-JD9X开发板

本篇测评由面包板论坛的优秀测评者“JerryZhen”提供。 本文将介绍基于米尔电子MYD-JD9X开发板打造成开源的Thingsboard网关。 Thingsboard网关是一个开源的软件网关,采用python作为开发语言,可以部署在任何支持 python 运行环境的主机上,灵…

前端将二维数组转换成树形格式数组

原始数据&#xff1a; 最终得到的数据&#xff1a; 具体实现代码&#xff1a;倒推&#xff08;最后一个数据&#xff09;的思路得到数组 function childData(list,i){const arr []//最后一个数组if(i<list.length){list[i].forEach(item>{const obj {name:,prop:,chil…

美摄视频特效SDK,视频特效技术解决方案

视频内容已经成为企业传播信息、提升品牌形象的重要手段。然而&#xff0c;普通的视频内容往往难以吸引观众的注意力&#xff0c;而富有创意的视频特效则能够大大提升视频的观赏性和吸引力。为了帮助企业轻松实现视频特效的添加&#xff0c;我们推出了美摄视频特效SDK&#xff…

风口下的低代码,怎么样了?

目录 一、前言 二、为什么低代码平台会受到资本的重视&#xff1f; 三、低代码的应用场景 1.基于表单/引擎驱动的模式 2.基于aPaaS平台的模式 四、低代码能解决什么问题&#xff1f; &#xff08;1&#xff09;运维工作流方向 &#xff08;2&#xff09;运维开发方向 五、低代码…

ArcGIS笔记10_如何创建渔网?

本文目录 前言Step 1 确定渔网的精度单位Step 2 有底图时创建渔网的操作 前言 ArcGIS中的渔网是一个很好用的工具&#xff0c;它可以创建出规规整整的小格子&#xff0c;每个小格子都对应一个标注点&#xff0c;可以将原本散乱的数据规整化&#xff0c;如下图&#xff1a; Ste…

Springboot-案例 增删改查二

准备 前端程序、后端工程&#xff08;web/mybatis/mysql/lombok&#xff09;、数据库 开发规范 GET&#xff1a;查询 POST&#xff1a;新增 PUT&#xff1a;修改 DELETE&#xff1a;删除 Result.java Data NoArgsConstructor AllArgsConstructor public class Result {pri…

如何正确地使用ChatGPT(角色扮演+提示工程)

如何正确地使用ChatGPT 一、ChatGPT介绍二、准备工作2.1 获取ChatGPT环境2.2 确定使用ChatGPT的目标和需求 三、重要因素3.1 角色赋予3.2 提示工程 四、正确案例 一、ChatGPT介绍 可以查阅ChatGPT快速入门 二、准备工作 2.1 获取ChatGPT环境 国外的有OpenAI和微软NewBing等…

如何快速绘制一张业务流程图?

产品经理在日常工作中经常需要和业务流程图打交道&#xff0c;它能很好地帮助我们梳理业务&#xff0c;高效表达需求&#xff0c;避免做无用功。 对于刚入门的PM来说&#xff0c;对业务流程图完全摸不着头脑的大有人在&#xff0c;今天从业务流程图的基本介绍、分类、业务流程…

超结MOS/低压MOS在微型逆变器上的应用-REASUNOS瑞森半导体

一、前言 微型逆变器&#xff0c;一般指的是光伏发电系统中的功率小于等于1000瓦、具组件级MPPT的逆变器&#xff0c;全称是微型光伏并网逆变器。“微型”是相对于传统的集中式逆变器而言的。传统的光伏逆变方式是将所有的光伏电池在阳光照射下生成的直流电全部串并联在一起&…

接物游戏demo

接物游戏demo&#xff1a; 接物元素设置了不同分值 指定时间内&#xff0c;接到元素则加分&#xff0c;接到炸弹则减分&#xff0c;计时结束&#xff0c;游戏停止 demo代码&#xff1a; <!DOCTYPE html> <html> <head> <meta charset"UTF-8"…

文心一言帮忙写代码之微信小程序图片移动顺序

先上效果图&#xff0c;图片顺序可移动&#xff0c;左移右移调准顺序。 代码是文心一言帮忙写的&#xff0c;自己稍微改造就可以用了。 首先是往左移&#xff0c;也就是从下标1移到下标0 &#xff0c;下标2移到下标1 var imglist [‘aa’, ‘bb’, ‘cc’, ‘dd’]; function…

智能电表上的模块发热正常吗?

智能电表是一种可以远程抄表、计费、控制和管理的电力计量设备&#xff0c;它可以实现智能化、信息化和网络化的电力用电管理。智能电表的主要组成部分包括电能计量模块、通信模块、控制模块和显示模块等。其中&#xff0c;通信模块和控制模块是智能电表的核心部件&#xff0c;…

C++11——多线程

目录 一.thread类的简单介绍 二.线程函数参数 三.原子性操作库(atomic) 四.lock_guard与unique_lock 1.lock_guard 2.unique_lock 五.条件变量 一.thread类的简单介绍 在C11之前&#xff0c;涉及到多线程问题&#xff0c;都是和平台相关的&#xff0c;比如windows和linu…

如何使用 NFTScan 的 TON API 实现 NFT 应用开发?

上周 NFTScan 开发者平台已上线了 TON 网络的 NFT API 服务&#xff0c;TON&#xff08;The Open Network&#xff09;是由 Telegram 团队打造的一条 Layer 1 高性能公链&#xff0c;采用自己的 TVM 虚拟机&#xff0c;与 EVM 不兼容。通过先进的技术架构实现每秒百万级 TPS 的…

为什么需要 SOME/IP

传统汽车协议的问题 如今的汽车不仅仅是通勤和交通工具&#xff0c;车辆现在设计得功能齐全&#xff0c;使旅程成为一种豪华体验。所有这些都是通过将先进的电子技术与车辆的发动机控制单元&#xff08;ECU&#xff09;集成而实现的。这种新的情景显著改变了ECU的不同单元之间…

生成扩散模型理论框架

1、随机微分方程&#xff08;Stochastic Differential Equation&#xff0c;SDE&#xff09; DDPM的一般化形式 2、常微分方程&#xff08;Ordinary differential equation&#xff0c;ODE&#xff09; DDIM的一般化形式 3、得分匹配&#xff08;Score matching&#xff09; …