【整洁单元测试】测试气味Test Smells

news2024/11/13 18:16:13

背景

"Code smell" 是软件开发中的一个术语,指的是代码中可能表明存在问题的某些迹象或模式。这些迹象本身并不表示代码一定有错误,但它们通常表明代码可能难以理解、维护或扩展。Code smells 可以视为一种警告,提示开发者需要进一步检查代码以确定是否存在更深层次的问题。

常见的 code smells 包括:

  1. 重复代码(Duplication):代码中有重复的逻辑或结构。
  2. 过长函数(Long Method):一个函数执行太多任务,难以理解和维护。
  3. 过大类(Large Class):类包含太多的属性和方法,违反了单一职责原则。
  4. 过长参数列表(Long Parameter List):函数或方法有过多的参数,难以使用。
  5. 数据泥团(Data Clumps):多个类或方法中出现相同的数据结构。
  6. 基本类型偏执(Primitive Obsession):过度使用基本数据类型,而不是创建更有意义的类。
  7. 切换/状态/类型(Switch/State/Type):使用大量的条件语句来处理不同的状态或类型。
  8. 霰弹式修改(Shotgun Surgery):修改代码时需要在多个地方进行更改。
  9. 特征嫉妒(Feature Envy):函数或方法似乎更关心另一个类的数据而不是自己的。
  10. 数据类(Data Class):类只包含数据和访问器,没有行为。

识别和解决 code smells 是重构过程的一部分,可以帮助提高代码质量和可维护性。

单元测试中代码味道

     在实际工作中,知道如何不编写代码可能与知道如何编写代码同样重要。测试代码也是如此;今天,我们将探讨编写单元测试时常见的错误。虽然编写单元测试是程序员的常见做法,但测试仍常常被视为二等代码。编写好的测试并不容易--就像在任何编程领域一样,有模式也有反模式。在 Gerard Meszaros 关于 xUnit 模式的书中,有一些关于测试气味的章节很有帮助,互联网上也有更多好东西。

一次测试的演变。 首先,我们要测试什么?一个原始函数:

public String hello(String name) {
     return "Hello " + name + "!";
}

我们开始为它编写单元测试:

@Test
void test() {

}

就这样,我们的代码已经有了味道。

1. 无信息的名称

当然,只写 test、test1、test2 要比写一个翔实的名称简单得多。而且,这样也更简短!

但是,写起来容易的代码远不如读起来容易的代码重要--我们花在阅读代码上的时间更多,而可读性差会浪费大量时间。名称应该传达意图;应该告诉我们正在测试什么。

也许我们可以把测试命名为 testHello,因为它测试的是 hello 函数?不行,因为我们不是在测试方法,而是在测试行为。所以好的名字应该是 shouldReturnHelloPhrase:

@Test
void shouldReturnHelloPhrase() {
     assert(hello("John")).matches("Hello John!");
}

除了框架之外,没有人会直接调用测试方法,因此名称太长也没有关系。它应该是一个描述性的、有意义的短语(DAMP)。

2. 没有 arrange-act-assert


名字还可以,但现在一行中塞进了太多的代码。最好把准备工作、我们要测试的行为和关于该行为的断言(rangement-act-assert)分开。

image

@Test
void shouldReturnHelloPhrase() {
     String a = "John";

    String b = hello("John");

    assert(b).matches("Hello John!");
}

在 BDD 中,习惯使用 Given-When-Then 模式,在本例中也是如此。

3. 变量名不正确,没有变量重复使用


但看起来还是写得很匆忙。a "是什么?b "是什么?你可以推断出一些,但想象一下,这只是测试运行中失败的几十个测试中的一个(在几千个测试的测试套件中完全有可能)。在对测试结果进行排序时,你需要做大量的推断工作!

因此,我们需要正确的变量名。

我们在匆忙中还做了一件事--我们所有的字符串都是硬编码的。硬编码某些内容是可以的,但必须与其他硬编码内容无关!

也就是说,当您阅读测试时,数据之间的关系应该是显而易见的。a' 中的 "John "与断言中的 "John "是否相同?在阅读或修复测试时,我们不应该在这个问题上浪费时间。

因此,我们可以这样重写测试:

@Test
void shouldReturnHelloPhrase() {
     String name = "John";

    String result = hello(name);
     String expectedResult = "Hello " + name + "!";

    assert(result).contains(expectedResult);
}

4. 杀虫剂效应


    这里还有一件事值得思考:自动化测试很好,因为你可以用很少的成本重复测试,但这也意味着随着时间的推移,它们的有效性会下降,因为你只是在重复测试完全相同的东西。这就是所谓的 "杀虫剂悖论"(Boris Beizer 在 20 世纪 80 年代创造的术语):虫子会对你用来杀死它们的东西产生抗药性。

要完全克服杀虫剂悖论可能是不可能的,但有一些工具可以通过在测试中引入更多的可变性来减少其影响,例如 Java Faker。让我们用它来创建一个随机名称:

@Test
void shouldReturnHelloPhrase() {
     Faker faker = new Faker();
     String name = faker.name().firstName();

    String result = hello(name);
     String expectedResult = "Hello " + name + "!";

    assert(result).contains(expectedResult);
}

好在我们在上一步中将名称改为了变量--现在我们不必再查看测试,找出所有的 "约翰 "了。

5. 信息不全的错误信息


      如果我们在匆忙中编写了测试,那么另一件我们可能没有考虑到的事情就是错误信息。在对测试结果进行分类时,您需要尽可能多的数据,而错误信息是最重要的信息来源。然而,默认的错误信息非常缺乏信息量:

java.lang.AssertionError at org.example.UnitTests.shouldReturnHelloPhrase(UnitTests.java:58)

太好了。我们唯一知道的就是断言没有通过。幸好,我们可以使用 JUnit`Assertions` 类中的断言。具体方法如下

@Test
void shouldReturnHelloPhrase4() {
     Faker faker = new Faker();
     String name = faker.name().firstName();

    String result = hello(name);
     String expectedResult = "Hello " + name + "";

    Assertions.assertEquals(
         result,
         expectedResult
     );
}

这是新的错误信息:

Expected :Hello Tanja! Actual :Hello Tanja

......这立刻告诉了我们出错的原因:我们忘记了感叹号!

经验教训
     

    这样,我们就有了一个很好的单元测试。我们能从这个过程中吸取什么教训呢?很多问题都是由于我们有点懒惰造成的。不是那种好的懒惰,你会认真思考如何减少工作量。而是坏的懒惰,即为了 "速战速决 "而走阻力最小的路。 硬编码测试数据、剪切和粘贴、使用 "test "+方法名称(或 "test1"、"test2"、"test3")作为测试名称,这些做法在短期内稍显简单,但却使测试库更难维护。一方面,我们一直在谈论测试的可读性和易读性,但同时却把一行测试变成了 9 行,这有点讽刺。不过,随着测试数量的增加,我们在此提出的做法将为您节省大量的时间和精力。

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

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

相关文章

0基础学python-14:python进阶之面向对象

前言 Python是一门解释型、面向对象以及动态数据类型的高级程序设计语言,今天所要说的就是极为重要的概念,面向对象。 一、面向对象的核心概念: 1.类(Class): 类是对象的抽象描述,是面向对象编…

HarmonyOS开发中几个常见问题

前言 最近开始HarmonyOS应用开发,遇到一些小问题,也算是自己看官网文档没记住的东西,过程中再记录一下。 一、更改应用的名字和图标 对比看下Android工程中是如何更改的,只需要在清单文件AndroidManifest.xml中,更改…

机器学习 | 深入理解激活函数

什么是激活函数? 在人工神经网络中,节点的激活函数定义了该节点或神经元对于给定输入或一组输入的输出。然后,将此输出用作下一个节点的输入,依此类推,直到找到原始问题的所需解决方案。 它将结果值映射到所需的范围…

CSS综合案例(快报模块头部制作)

(大家好,今天我们将继续来学习CSS的相关知识,大家可以在评论区进行互动答疑哦~加油!💕) 目录 一、前述 二、案例分析 1.样例参看 2.拆分分析 三、案例实施 一、前述 案例:快报模块头部制…

微信小程序基本语法

官网 https://developers.weixin.qq.com/miniprogram/dev/framework/ 视频教程:尚硅谷微信小程序开发教程,2024最新微信小程序项目实战! 仿慕尚花坊项目源码:https://gitee.com/abcdfdewrw/flower-workshop 目录 一,初…

热门软件缺陷管理工具2024:专业评测与建议

国内外主流的10款软件缺陷管理工具软件对比:PingCode、Worktile、禅道、Tapd、Teambition、Tower、JIRA、Bugzilla、MantisBT、Trac。 在软件开发过程中,管理缺陷和漏洞常常成为一项挑战,尤其是在项目规模庞大时。选择一个高效的软件缺陷管理…

C#实现自定义标签的设计和打印

背景:最近在进行资产盘点的时候,需要对固定资产设计标签并进行打印。 设计标签:选用的是Fastreport自带的,可拆包忌用的标签设计器;进行标签的模型设计。 软件解压后可直接进行使用。模板的设计基本都是无脑操作,拖拽控件按,放置到固定未知即可;我设计的模板如下: 说…

Vision Pro的增强视觉:企业级Unity插件包实现主摄像头访问

在AR和VR技术的快速发展中,Unity作为跨平台游戏和应用开发的首选引擎,其插件生态的丰富性一直是开发者们关注的焦点。最近,一个专为Vision Pro设计的Unity插件包——EnterpriseCameraAccessPlugin,因其能够通过企业API访问主摄像头的功能,引起了广泛关注。 一、插件背景与…

springboot+vue+mybatis鲜花管理系统+PPT+论文+讲解+售后

随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,鲜花管理系统当然也不能排除在外。鲜花管理系统是以实际运用为开发背景,运用软件工程开发方法,采用SSM技…

小阿轩yx-zookeeper+kafka群集

小阿轩yx-zookeeperkafka群集 消息队列(Message Queue) 是分布式系统中重要的组件 通用的使用场景可以简单地描述为 当不需要立即获得结果,但是并发量又需要进行控制的时候,差不多就是需要使用消息队列的时候。 消息队列 什么是消息队列 消息(Mes…

前端框架入门之Vue _el和data的两种写法 分析MVVM模型

目录 _el与data的两种写法 MVVM模型 _el与data的两种写法 查看vue的实例对象 我们在这边注释掉了el属性 这样的话div容器就绑定不了vue实例 当我们可以在这里写一个定时任务 然后再回头指定 这个mount有挂载的意思 就是把容器对象交给vue实例后 去给他挂载指定的对象 &…

MySQL 进阶(四)【锁】

1、锁 1.1、锁的概述 锁就不需要多介绍了,多个用户访问共享数据资源,如何保证数据并发访问的一致性、有效性是数据库最重要的问题。同时,锁冲突也是影响一个数据库并发性能最重要的因素。 MySQL 中锁的划分有三类: 全局锁&…

敏捷营销在AI智能名片微信小程序中的应用探索

摘要:在数字化转型的浪潮中,企业面临着前所未有的挑战与机遇。AI智能名片微信小程序作为一种创新的营销工具,以其便捷性、智能化和高效性,正逐步成为企业连接客户、推广品牌的新宠。然而,如何在快速变化的市场环境中&a…

Automation Anywhere推出新一代AI+自动化企业系统,助力企业实现10倍商业增长

RPA厂商纷纷进军AI Agent ( AI 代理)领域,陆续推出创新产品。最近,Automation Anywhere宣布推出其新的AI 自动化企业系统,该系统结合AI和自动化技术,以实现指数级的业务成果。 在Imagine 2024大会上首次亮相的这款新产品&#xf…

机器学习中的梯度下降

本文只是简单解释一下梯度下降,其中涉及到的公式并没有展示说明。 1.什么是梯度? 梯度也可以理解为导数。 在一维空间中:梯度就是导数,或者说对于一个线性函数,也就是线的斜率。 2.什么是梯度下降? 梯度是…

字典树实现

一、字典树 字典树(Trie树)是一种多叉树结构,每条边代表一个字符,从根节点到其它节点的路径构成一个单词。其具有较好的查询性能,可以用于有效地存储大量字符串,并支持高效的查找、插入和删除操作。 二、…

浏览器缓存:强缓存与协商缓存实现原理有哪些?

1、强缓存:设置缓存时间的,那么在这个时间内浏览器向服务器发送请求更新数据,但是服务器会让其从缓存中获取数据。 可参考:彻底弄懂强缓存与协商缓存 - 简书 2、协商缓存每次都会向浏览器询问,那么是怎么询问的呢&…

java 项目使用 acitiviti 流程引擎中的人员设置

学习目标: 目标 [ ]了解 java 项目使用 acitiviti 流程引擎中的人员设置 知识小记: - [x] 1、人员选择说明 - [x] 2、分配任务候选人 任务的候选人是指有权限对该任务进行操作的潜在用户群体,这个用户群体有权限处理(处理、完成)该任务…

第九课:服务器发布(静态nat配置)

一个要用到静态NAT的场景,当内网有一台服务器server1,假如一个用户在外网,从外网访问内网怎么访问呢,无法访问,这是因为外网没办法直接访问内网,这时候需要给服务器做一个静态NAT。 静态NAT指的是给服务器…

学习笔记——动态路由——IS-IS中间系统到中间系统(特性之路由泄漏)

3、路由泄漏 什么是路由泄漏? IS-IS路由协议允许路由信息的两级层次结构。可以有多个1级区域通过连续的2级主干互连。路由器可以属于1级、2级或两者。1级链路状态数据库仅包含有关该区域的信息。第2级链路状态数据库包含有关该级别以及每个第1级区域的信息。L1/L2…