如何培养单元测试的习惯?怎样才算一个好的单元测试?

news2024/11/15 20:39:51

你是怎么编写单元测试的呢?很多人的做法是先把所有的功能代码都写完,然后,再针对写好的代码一点一点地补写测试。

在这种编写测试的做法中,单元测试扮演着非常不受人待见的角色。你的整个功能代码都写完了,再去写测试就成了一件为了应付差事不得不做的事情。更关键的一点是,你编写的这些代码可能是你几天的工作量,你已经很难记得在编写这堆代码时所有的细节了,这个时候补写的测试对提升代码质量的帮助已经不是很大了。

所以,想要写好单元测试,最后补测试的做法总是很糟糕的,仅仅比不写测试好一点。你要想写好单元测试的话, 最好能够将代码和测试一起写。

你或许会说,我在功能写完后立即就补测试了,这不就是代码和测试一起写的吗?其中的差异在于,把所有的功能写完的这个粒度实在是太大了。为一个大任务编写测试,是一件难度非常大的事,这也是很多人觉得测试难写的重要因素。要想做好单元测试,关键就是工作的粒度要小。

I’m not a great programmer; I’m just a good programmer with great habits.

我不是一个伟大的程序员,只是一个有着好习惯的优秀程序员。

—— Kent Beck

任务分解是每个程序员都应该拥有的好习惯,即便你 想写好单元测试也要从任务分解开始。所以,你需要把一个要完成的需求拆分成很多颗粒度很小的任务。粒度要小到可以在很短时间内完成,比如,半个小时就可以写完。只有能够把任务分解成微操作,我们才能够认清有足够的心力思考其中的每个细节。千万不要高估自己对于任务把控的粒度, 一定要把任务分解到非常小,这是能够写好代码,写好测试的前提条件,甚至可以说是最关键的因素。

当我们把需求拆分成颗粒度很小的任务时,我们才开始进入到编码的状态。而从这里开始,我们进入到代码和测试一起写的状态。

编写单元测试的过程

对于一个具体的任务,我们首先要弄清楚的是,怎么样算是完成了。一个完整的需求我们需要知道其验收标准是什么。具体到一个任务,虽然没有业务人员给我们提供验收标准,我们自己也要有一个验收标准,我们要能够去衡量怎么样才算是这个代码写合格了。

经过我们这一系列关于测试的介绍,你应该已经知道我要说什么了:一个任务的代码要通过测试才算编码阶段的完成。

但测试用例从哪来呢?这就需要我们设计了。不同于业务测试的测试用例,我们现在要写的是单元测试。而我们要测的单元现在还没有写,所以,没有人会给我们提供测试用例,单元测试的用例只能我们自己来。

还记得我们在实战里怎么做的添加 Todo 项吗?接下来,我们就结合这个部分来谈谈具体怎么做。

我们首先要确定的是待测单元的行为,也就是要实现的类里的一个函数,它的行为是什么样的。或许你已经发现了,这其实就是一个软件设计的过程。这里的设计指的是微观的设计,就是具体的一个函数准备写成什么样子。通常到了动手写代码这一步,大的设计已经在前面做完了。

因为我们现在不仅仅要写代码,还要写测试。所以,我们在设计这个函数接口时,还必须增加一点考量:它要怎么测。

在添加一个 Todo 项时,我们经过设计出来的函数接口就是下面这样。

TodoItem addTodoItem(final TodoParameter todoParameter);

有了一个具体的函数接口设计,我们就可以针对它进行更具体的测试用例设计,也就是设计测试用例来描述这个接口的行为。

是的,这里我们并没有着急写代码。对很多人来说,写代码的优先级很高,但是,如果不在这里停一下的话,你可能就不会去思考是否还有要考虑的问题,而是直奔代码细节去了。而当我们专注于细节时,有限的注意力就会让你忽略掉很多东西。所以, 先设计测试用例,后写代码,这是一个编码习惯的问题。

有了添加 Todo 项接口之后,我们就准备了两个测试场景:

添加正常的参数对象,返回一个创建好的 Todo 项;

有了测试场景,接下来把这些场景实例化出来,这个步骤相对来说就比较简单了。比如,对于添加正常的参数对象来说,那什么样的参数对象是正常的?我们就代入一个具体的正常参数(比如 foo)。有了这个实例化过的参数,我们就可以把具体的测试用例表现出来了。


  @Test

  public void should_add_todo_item() {

   TodoItemRepository repository = mock(TodoItemRepository.class);

   when(repository.save(any())).then(returnsFirstArg());

   TodoItemService service = new TodoItemService(repository);

   TodoItem item = service.addTodoItem(new TodoParameter("foo"));

   assertThat(item.getContent()).isEqualTo("foo");

  }

在实际的工作中,究竟是先写测试,还是先写实现代码,这是个人工作习惯的问题。当我们有了测试用例之后,其实就是把一个具体的任务进一步拆分成更小的子任务了。只要我们完成一个子任务,我们就可以做一次代码的提交,因为我们这个时候,既有测试代码又有实现代码,而且实现代码是通过了测试的。

测接口还是测实现?

不知道你是否注意到了,在前面我一直在说,我们要测的是函数接口的行为。我一直说,单元测试是一种白盒测试。在一些人的理解中,白盒测试的关注点应该是内部实现。那单元测试到底应该关注接口,还是应该关注实现呢?

或许你还不清楚二者之间的区别,让我们把前面添加 Todo 项的例子拿过来。如果采用更加面向实现的做法,我们应该对 addTodoItem 这个函数的内部实现有进一步的约束,就像下面这样。


  @Test

  public void should_add_todo_item() {

   TodoItemRepository repository = mock(TodoItemRepository.class);

   when(repository.save(any())).then(returnsFirstArg());

   TodoItemService service = new TodoItemService(repository);

   TodoItem item = service.addTodoItem(new TodoParameter("foo"));

   assertThat(item.getContent()).isEqualTo("foo");

   verify(repository).save(any());

  }

这段代码中核心的差别就是增加了一句 verify,这也就意味着,我规定在 addTodoItem 的实现中必须要调用 repository 的 save 函数。

你或许会好奇,repository 本来就要调用 save 方法,那我在这里校验它调用了 save 方法,似乎也没什么大不了的。

单独这么看确实看不出什么问题,但是,如果你有很多测试都是这么写,当你准备重构时,你就会发现问题了。很多团队代码一调整,测试就失败,一个重要的原因就是代码实现和测试之间紧紧地绑定在了一起。因为测试约束的是实现细节,而只要调整实现细节,测试当然就失败了。这也是很多团队抱怨单元测试问题很多的重要原因。

所以, 在实际的项目中,我会更倾向于测试接口,尽可能减少对于实现细节的约束。其实,这个原则不仅仅是在接口层面上,在一些测试的细节上也可以这么约定,比如下面这行代码。

when(repository.save(any())).then(returnsFirstArg());

这其实是一种宽泛的写法,所以用了 any。如果严格限制的话,应该严格限定一个非常具体的参数。

  when(repository.save(new TodoItem("foo"))).then(returnsFirstArg());

使用 Moco框架,我们设置模拟服务器可以设置得非常具体,像下面这样。​​​​​​​


  server

   .request(and(by("foo"), by(uri("/foo"))))

   .response(and(with(text("bar")), status(200)));

也可以设置得非常宽泛,像这样。

server.request(by(uri("/foo"))).response("bar");

除非这个测试里面有多个类似的请求,必须要做区分,否则,我倾向于使用宽泛一些的约束。这在某种程度上会降低未来重构代码时带来的影响。

不过实话说,要想完全消除对于实现细节的依赖,有时候也是很难的。比如在我们前面的 TodoItemService 的例子里面,repository 本身也是 TodoItemService 的一种实现细节,一旦进行一些重构,把 repository 的依赖从 TodoItemService 中拿掉,很多测试代码也需要调整。所以,在实际的项目中,我们只能说尽可能减少对于实现细节的依赖。

其实,关于实现细节的测试也是一种重复,等于你用测试把代码又重新写了一遍。程序员的工作中有一种重要的原则:DRY(Don’t Repeat Yourself),这不仅仅是说代码中不要有重复,而且各种信息都不要重复。

总结

很多团队由于多方面的原因(比如设计做得不好),导致单元测试写得少。但为了提高代码质量以及更准确地定位问题,我们应该多写单元测试。

单元测试最好是和实现代码一起写,以便减少后续补测试的痛苦。想写好测试,关键要做好任务分解,否则,面对一个巨大的需求,没有人知道如何去给它写单元测试。

编写单元测试的过程,实际上就是一个任务开发的过程。一个任务代码的完成,不仅仅是写了实现代码,还要通过相应的测试。一般而言,任务开发要先设计相应的接口,确定其行为,然后根据这个接口设计相应的测试用例,最后,把这些用例实例化成一个个具体的单元测试。

单元测试常见的一个问题是代码一重构,单元测试就崩溃。这很大程度上是由于测试对实现细节的依赖过于紧密。一般来说,单元测试最好是面向接口行为来设计,因为这是一个更宽泛的要求。其实,在测试中的很多细节也可以考虑设置得宽泛一些,比如模拟对象的设置、模拟服务器的设置等等。

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2057589.html

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

相关文章

C语言:顺序表的实现和通迅录项目实现

顺序表 1、顺序表的概念及结构 1.1 线性表 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是⼀种在实际中⼴泛使⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构&…

openstack基本操作

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

python语言day9 正则表达式 和 xpath 解析html

正则表达式&#xff1a; 正则表达式的语法&#xff1a; 元字符&#xff1a; \D \d \w \W . [ ] 量词&#xff1a; ? * 惰性匹配&#xff1a; 玩儿(?P<name>.*?)游戏&#xff1a; 匹配到第一个游戏结束&#xff0c;name 匹配的文本。 玩儿(?P<na…

分享cesium的风场开源网站

首先是有在二维地图上的一个风场效果&#xff0c;通过canvas进行的绘制&#xff0c;例如leaflet开源地图上就能够根据数据生成风场的效果图。 最近mapbox里的大神分享了如何在cesium上实现风场的效果&#xff0c;并在github上进行了开源&#xff0c;开源地址: https://github.…

SSL Pining 问题解决方案

实战案例 为了能够更好的复现 SSL Pining 场景&#xff0c;我们对一个 App&#xff08;https:app4.scrape.center&#xff09;进行抓包&#xff0c;这个 App 包含了 SSL Pining 的相关设置&#xff0c;如果我们将手机的代理设置为抓包软件提供的代理服务&#xff0c;那么这个 …

Windows 11上RTX 4090深度学习与大模型微调环境安装指南

【本文原作者&#xff1a;擎创科技资深产品专家 布博士】 在安装深度学习及大模型微调环境时&#xff0c;经历了多次反复操作&#xff08;如CUDA、cuDNN、PyTorch的安装与卸载&#xff09;。为了避免走弯路&#xff0c;总结了以下步骤&#xff1a; 步骤 1&#xff1a;显卡驱动…

【轨物方案】直流电源屏物联网解决方案,让在线监测更简单!

流屏是保证各类变电站、水力、火力发电厂正常、安全运行的电源设备&#xff0c;也是其它使用直流设备用户(如石化、矿山、铁路、医院等)的直流电源&#xff0c;是电力系统的重要组成部分。同时直流屏在变配电中&#xff0c;发挥着很大的作用&#xff0c;它在很大程度上影响着配…

Waymo第六代无人驾驶技术亮相:更少传感器,更高效率

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

【系统分析师】-综合知识-数据库基础

1、给定关系模式 R < U &#xff0c;F >&#xff0c; U {A&#xff0c;B&#xff0c;C&#xff0c;D &#xff0c;E} &#xff0c; F {B→A &#xff0c;D→A &#xff0c;A→E &#xff0c;AC→B }&#xff0c;则 R 的候选关键字为&#xff08;CD&#xff09;&#xff…

76、docker-harbor

一、docker-harbor 私有仓库部署和管理&#xff1a; docker-harbor 私有仓库部署和管理&#xff1a; harbor&#xff1a;开源的企业级的docker仓库软件。 仓库&#xff1a;私有仓库、公有仓库。私有仓库。 docker-harbor&#xff1a;是有图形化的&#xff0c;页面UI展示的一…

1.XV6环境配置

安装虚拟机 这个就不多说了&#xff0c;搞一台Ubuntu虚拟机即可&#xff0c;最好是通过vscode 用ssh远程连接进行实验会比较方便&#xff0c;具体怎么做可参考我这篇博客&#xff1a; VsCode配置SSH连接远程服务器&#xff08;手把手&#xff0c;学不会打我&#xff09;_vsco…

【Hot100】LeetCode—148. 排序链表

目录 1- 思路归并 2- 实现⭐148. 排序链表——题解思路 3- ACM 实现 原题连接&#xff1a;148. 排序链表 1- 思路 归并 1- 先求解链表的长度&#xff0c;求出长度后利用 subLen 1 开始归并 定义虚拟头结点 dummyHead &#xff0c;便于处理头结点 2- 归并逻辑 for(int subLen…

nacos 使用 docker 单机部署连接 MySQL 数据库并开启鉴权

文章目录 本地部署的配置启用鉴权(未验证) docker部署的配置修改docker 镜像源启用鉴权&#xff0c;必须添加如下环境变量如何生成鉴权的密钥 完整环境变量docker启动命令 本地部署的配置 文件结构 application.properties #配置文件 mysql-schema.sql …

[Linux#40][线程] 线程控制 | 多线程

内核中有没有很明确的线程概念呢&#xff1f;没有的。有的是轻量级进程的概念 不会给我直接提供线程的系统调用&#xff0c;只会给我们提供轻量级进程的系统调用&#xff0c;但是我们用户&#xff0c;需要线程的接口&#xff01; 所以 Linux 开发者提供了 pthread 线程库--应用…

成为创作者的第1024天:成长与技术积累的旅程

前言 &#x1f4eb; 大家好&#xff0c;我是南木元元&#xff0c;热爱技术和分享&#xff0c;欢迎大家交流&#xff0c;一起学习进步&#xff01; &#x1f345; 个人主页&#xff1a;南木元元 今天是我成为创作者的第1024天。回顾这段时间&#xff0c;虽然日常的忙碌充斥着生活…

roles(角色)

创建目录&#xff0c;编写剧本下载nginx: 184 mkdir /etc/ansible/playbook 185 vim /etc/ansible/playbook/nginx.yml --- - hosts: groupremote_user: roottasks:- name: 卸载httpdyum: namehttpd stateabsent- name: 安装nginxyum: …

【MySQL 09】复合查询 (带思维导图)

文章目录 &#x1f308; 一、准备工作&#x1f308; 二、多表查询⭐ 1. 多表笛卡尔积⭐ 2. 多表查询示例 &#x1f308; 三、自连接&#x1f308; 四、子查询⭐ 1. 标量子查询⭐ 2. 多行子查询 (需要插入其他博客的链接)⭐ 3. 多列子查询 (需要插入其他博客的链接)⭐ 4. 在 fro…

小米SU7销量超特斯拉,新车明年上半年发布

小米 SU7&#xff0c;一款国内新能源车品牌纯血新势力旗下首款轿车&#xff0c;上市短短 4 个月卖出超 4 万台&#xff0c;月均销量过万。 该说不说&#xff0c;这放在整个新能源汽车工业史上也足以称得上是一件小刀喇拍屁股&#xff0c;让人开了眼的事儿。 就在本月初&#x…

大模型在企业数智化转型中可以做哪些事情?

在数字化浪潮的推动下&#xff0c;企业数智化转型已成为不可逆转的趋势。作为人工智能技术的集大成者&#xff0c;大模型以其强大的数据处理能力、深度学习能力及广泛的应用场景&#xff0c;正逐步成为企业数智化转型的核心驱动力。 大模型&#xff1a;智能时代的基石 大模型…

Error: ReferenceError: ReadableStream is not defined

midway项目在build完&#xff0c;docker启动时&#xff0c;莫名地报错Error: ReferenceError: ReadableStream is not defined&#xff0c;之前一直好好地&#xff0c;初时以为是新加的代码引起&#xff0c;后来排除了。 报错如下&#xff1a; 2024-08-20 11:57:51.446 ERROR …