你在测试金字塔的哪一层(下)

​在《你在测试金字塔的哪一层(上)》中介绍了自动化测试的重要性以及测试金字塔。测试金字塔分为单元测试、服务测试、UI测试,它们分别是什么呢?本期文章让我们一起详细看看测试金字塔的不同层次。

测试金字塔-1

一、单元测试

单元测试是指对程序模块(软件设计的最小单位)进行正确性检验的测试工作,能够提高代码质量和可维护性。

但对“一个单元”的概念是没有标准答案,每个人可以根据自身所处的编程范式和语言环境确定。在函数式语言中,一个函数可以被视为一个单元,其单元测试涉及使用不同的参数调用该函数,并断言其返回了期待的结果。而在面向对象语言里,下至一个方法,上至一个类都有可能视为一个单元。

单元测试的一个重要好处在于我们可以为所有的产品代码类写单元测试,不需要在意它们的功能或者它们在内部结构中所处的层次。我们可以对controller进行单元测试,也可以用同样的方式对repository、领域类或文件读写类进行单元测试。一个良好的开端始于坚持一个实现类对应一个测试类的原则。

一个好的单元测试类至少应该测试该类的公共接口,因为私有方法无法直接进行测试。受保护的和包私有的方法可以被测试类直接调用(如果测试类和生产代码类的包结构相同),但是测试这些方法可能会过于以来实现细节。

编写单元测试有一条准则:测试应该覆盖代码的所有路径,包括正常路径和边缘路径,同时不与代码的实现有过于紧密的耦合。如果测试与产品代码耦合太紧密,这可能失去单元测试作为代码变更保护网的好处,这会导致每次重构测试的失败,给测试人员增加额外的工作量。因此,我们应该测试可观察的行为,而不是过于依赖实现的内部结构。

在编写单元测试时,我们需要思考:

如果我得输入是X和Y,输出会是Z吗?

而不是这样:

如果我的输入是x和y,那么这个方法会先调用A类,然后调用B类,接着输出A类和B类返回值相加的结果吗?

私有方法应该被视为实现细节。有人认为,单元测试是毫无意义的工作,为了获得高测试覆盖率就必须测试所有方法,包括getter、setter等琐碎的代码。
但这个观点是错误的。我们确实需要测试公共接口,但重要的是不要测试微不足道的代码。这些代码不会带来任何价值,应该节省时间开始其他有意义的工作。

如果你发现自己陷入测试私有方法的困境中,先问问自己为什么需要测试私有方法。很可能是一个设计问题,而不仅仅是方法可见性的问题。可能是因为方法过于复杂,如果通过公共接口来测试它,需要准备大量的数据和环境。

在这种情况下,可以考虑将原来的类拆分成两个类,按照职责进行拆分。将原来急于测试的私有方法移到新的类中,然后让旧类调用新类上的方法。这样,原来难以测试的私有方法就变成了公共方法,可以轻松添加测试。同时,这种重构还改善了代码结构,符合单一职责原则。

一个好的测试结构是这样的:

  • 准备测试数据

  • 调用被测方法

  • 断言返回的是你期待的结果

有一个口诀可以帮你记住这种结构:“Arrange、Act、Assert”。另一个口诀则是从BDD获取的灵感:“given、when、then”,即given是准备数据,when是调用方法,then是断言。

这种模式不仅适用于单元测试,还可以应用于其他更高层次的测试。在任何情况下,这种测试结构都能让测试保持一致,且易于阅读。此外,使用这种结构写出来的测试往往更加简短、更具表达力。

在明确了要测试什么以及如何组织单元测试后,我们可以看一个简化版的ExampleController类:

@RestController
public class ExampleController {private final PersonRepository personRepo;@Autowiredpublic ExampleController(final PersonRepository personRepo) {this.personRepo = personRepo;}@GetMapping("/hello/{lastName}")public String hello(@PathVariable final String lastName) {Optional foundPerson = personRepo.findByLastName(lastName);return foundPerson.map(person -> String.format("Hello %s %s!",person.getFirstName(),person.getLastName())).orElse(String.format("Who is this '%s' you're talking about?",lastName));}
}

一个针对hello(lastname)方法的单元测试可能是这样的:

public class ExampleControllerTest {private ExampleController subject;@Mockprivate PersonRepository personRepo;@Beforepublic void setUp() throws Exception {initMocks(this);subject = new ExampleController(personRepo);}@Testpublic void shouldReturnFullNameOfAPerson() throws Exception {Person peter = new Person("Peter", "Pan");given(personRepo.findByLastName("Pan")).willReturn(Optional.of(peter));String greeting = subject.hello("Pan");assertThat(greeting, is("Hello Peter Pan!"));}@Testpublic void shouldTellIfPersonIsUnknown() throws Exception {given(personRepo.findByLastName(anyString())).willReturn(Optional.empty());String greeting = subject.hello("Pan");assertThat(greeting, is("Who is this 'Pan' you're talking about?"));}
}

二、集成测试

常见的应用通常需要与外部环境进行集成,如数据库,文件系统等。为了更好地隔离测试并提高运行速度,我们通常在写单元测试时不涉及这些外部依赖。不过,这些交互始终是存在的,需要进行测试覆盖。这正是集成测试的用途,是应用与所有外部依赖的集成。

对于自动化测试来说,不仅需要运行应用本身,还需要运行与之集成的组件。如果要测试与数据库的集成,就需要在与运行测试时启动数据库。如果要测试从硬盘里读取文件的功能,就需要先在集成测试种保存一个文件到硬盘上,然后进行读取测试。

前面我提到过「单元测试」是一个模糊的术语,集成测试也是如此。我对集成测试更加狭义:每次只测试一个集成点。在进行测试时,我们使用测试替身来代替其他的外部服务、数据库等。同时,使用契约测试来覆盖测试替身和真实实现之间的约定。这样进行的集成测试更快、更独立、更易理解和调试。

狭义的集成测试主要测试是服务的边界。从概念上来说,这种测试总是在触发应用与外部依赖(如文件系统、数据库、其他服务等)进行集成的行为。例如,一个数据库集成测试可能按照以下步骤进行:

  • 启动数据库

  • 连接应用到数据库

  • 调用被测函数,该函数会往数据库写数据

  • 读取数据库,查看期望的数据是不是被写到了数据库里

另一个例子是通过REST API和外部服务集成的测试,可能会这样写:

  • 启动应用

  • 启动一个被测外部服务的实例(或者一个具有相同接口的测试替身)

  • 调用被测函数,该函数会从外部服务的API读取数据

  • 检查应用是否能正确解析返回结果

集成测试同样可以写得很白盒。一些框架在应用启动后,仍然支持对应用的某些部分进行mock,我们可以验证正确的交互是否发生。

代码中所有涉及数据序列化和反序列化的地方都要写集成测试,保证了对外部系统的数据读写操作的正常行。这些场景可能比你想象得更多,比如说:

  • 调用自身服务的 REST API

  • 读写数据库

  • 调用外部服务的 API

  • 读写队列

  • 写入文件系统

编写狭义的集成测试时,我们应尽可能在本地运行外部依赖,如启动本地的MySQL数据库、针对本地的ext4文件系统进行测试等。如果是与外部服务集成,可以在本地运行该服务的实例,或构建一个在本地运行的模拟真实服务的假服务。

对于无法在本地运行实例的某些第三方服务,可以考虑运行一个专用实例,并在集成测试中指向该实例。这能避免在自动化测试种集成真实的生产环境的服务。在生产环境种生成大量的测试请求可能会干扰日志记录,最坏的情况可能是对该服务产生DoS攻击。通过网络与服务集成是广义集成测试的一大特征,这会导致测试更慢、更难编写。

在测试金字塔中,集成测试的层级比单元测试更高。与隔离了外部依赖的单元测试相比,集成测试通常需要更长的时间来处理缓慢的外部依赖(如文件系统或数据库等)。这可能更难写,因为我们需要确保外部依赖在测试中正常运行,但它们的优势在于建立对应用正确访问外部依赖的信心,这是纯粹的单元测试无法做到的。

PersonRepository是代码里唯一的数据库类。它依赖于Spring Data,我们并没有实际实现它。只需要继承CrudRepository接口并声明一个方法名,剩下的就是Spring魔法了,Spring会帮我们实现其他所有的东西。

public interface PersonRepository extends CrudRepository {Optional findByLastName(String lastName);
}

Spring Boot提供了完整的CRUD方法,例如findOne,findAll,save,update和delete。我们自定义的方法(findByLastName())继承了这些基础功能并实现了根据last name获取Persons对象的功能。Spring Data会解析方法的返回类型,按照命名规范解析方法名,从而决定如何实现这些方法。

尽管Spring Data已经实现了与数据库的交互功能,但我认为需要写一个数据库集成测试。首先,它测试了我们自定义的findByLastName方法是否按预期工作。其次,它证明了我们的数据库类正确地使用了Spring的装配特性,并且能够正确地连接到数据库。

我们在本地运行测试,无需真的安装PostgreSQL数据库,而是连接到一个内存H2数据库,这可以提供更简单的环境设置。我们在build.gradle中已经将H2定义为测试依赖项。在测试目录下的application.properties文件中没有定义任何spring.datasource属性,这会告诉Spring Data使用内存数据库,并在classpath中找到H2运行测试。

当我们真正启动应用时,可以使用int profile(如把SPRING_PROFILES_ACTIVE=int设置为int),它会连接到application-int.properties里定义的PostgreSQL数据库。

除此以外,使用内存数据库进行测试实际上是有风险的。毕竟,集成测试针对的数据库和我们生产用的数据库是不同。下面是一个集成测试的示例,它先将一个Person对象保存到数据库中,根据last name查找。

@RunWith(SpringRunner.class)
@DataJpaTest
public class PersonRepositoryIntegrationTest {@Autowiredprivate PersonRepository subject;@Afterpublic void tearDown() throws Exception {subject.deleteAll();}@Testpublic void shouldSaveAndFetchPerson() throws Exception {Person peter = new Person("Peter", "Pan");subject.save(peter);Optional maybePeter = subject.findByLastName("Pan");assertThat(maybePeter, is(Optional.of(peter)));}
}

三、UI测试

大多数应用都有用户界面,特别是在web应用的上下文中,我们所谈的界面就是指网页界面。但人们常常忽视除了多彩的网页页面,还有许多的REST API界面、命令行界面等。

UI测试的目标是验证应用的用户界面是否按预期工作。例如,用户的输入要触发正确的动作、数据要能正确展示给用户、UI的状态要发生正确变化等。

大家有时候会将UI测试和端到端测试混为一谈。诚然,端到端测试通常包含了许多UI测试。但UI测试不必非得通过端到端的方式完成。根据技术栈不同,有时UI测试可以很简单,只需要为前端的JavaScript代码写一些单元测试,同时用桩(stub)将后端隔离开即可。

对于网页界面而言,UI可以围绕这些部分测试:行为、布局、可用性以及少数人认为需要测试的设计一致性。测试应用的布局是否前后一致确实则有些困难。由于应用类型和用户需求的不同,我们需要确保代码的更改不会意外破坏页面的布局。众所周知,计算机在判断某物「看起来是否不错」方面一直表现不佳。

当我们想测试可用性或一些「看起来对不对」的东西时,就已经超越了自动化测试的范畴。这属于探索性测试、可用性测试、走廊测试的领域。我们需要向用户展示产品,观察他们是否喜欢使用,是否有任何功能会让他们在使用时感到困惑。

通过用户界面测试一个已部署好的应用,这是一个典型的端到端测试(也被称为广域栈测试)。端到端测试会让我们更了解软件能否正常工作,然而它们通常比较脆弱,经常因为一些意料之外的问题而失败,并且错误信息通常不是真正的根本原因。浏览器差异、时间(时序)问题、元素渲染、意外的弹出框…这些问题仅仅是冰山一角,但却需要花费大量时间进行调试。

在微服务的世界中,谁负责写这些测试是一个大问题。因为端到端测试覆盖到整个服务,这就导致写端到端测试并不是任何一个团队的责任。

如果有一个集中的质量保障团队来编写端到端测试,这似乎是个不错的选择。但是,拥有一个集中式的QA团队实际上是一种反模式,不符合DevOps的理念。您的团队应该是真正的跨职能团队。回答谁应该负责端到端测试的问题并不容易,这与您的组织具体情况相关。也许您的组织中有一些社区实践或质量协会等机构可以负责这方面的工作。合适的答案与您的组织有关。

此外,端到端测试需要大量的维护成本,且运行速度较慢。试想一下,除非只有几个微服务,否则根本没办法在本地运行端到端测试,因为这需要启动所有的服务。

由于维护成本高昂,我们应该尽量将端到端测试的数量减少到最低限度。考虑到应用中对用户而言具有高价值的交互,并定义产品核心价值的用户旅程,将这些旅程中最重要的步骤转化为自动化的端到端测试。

例如,如果您正在构建一个电子商务网站,最有价值的用户旅程可能是用户搜索商品、将其添加到购物车,然后进行付款。只要这个旅程正常工作,您就无需过多担心。您可以找出一两个重要的用户旅程,并使用端到端测试来覆盖它们。但是,不要过度测试,否则会带来痛苦。

四、写在最后

请记住,在测试金字塔中,还有许多更低层级的测试,它们已经全面测试了各种边缘情况和与其他系统的集成。不需要在高层级测试中重复测试。否则,高维护成本和大量虚假错误报告将降低开发速度,最终会让您对测试失去信心。

文章翻译来源:Ham Vocke 的《The Practical Test Pyramid》

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

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

相关文章

web使其盒子向里面倾斜

右侧向内倾斜 transform: perspective(500px) rotateX(0deg) rotateY(-10deg) rotateZ(0deg);transform-origin: 270px 424px;效果 左侧向内倾斜 transform: perspective(500px) rotateX(0deg) rotateY(10deg) rotateZ(0deg);transform-origin: 270px 424px;效果

蓝桥杯小白月赛第八场第三题

题目描述: 思路: 根据上面的次方数,我们可以看出来从1次方到4次方 和 5 - 8次方,中间有什么规律? 是不是可以看出来1次方和5次方的尾数相同 2次方和6次方的尾数相同 3次方和7次方的尾数相同 4次方和8次方的尾数相同 …

2024年N1叉车司机证考试题库及N1叉车司机试题解析

题库来源:安全生产模拟考试一点通公众号小程序 2024年N1叉车司机证考试题库及N1叉车司机试题解析是安全生产模拟考试一点通结合(安监局)特种作业人员操作证考试大纲和(质检局)特种设备作业人员上岗证考试大纲随机出的…

Kaggle注册验证码问题(Captcha must be filled out.)

Kaggle注册验证码问题 Captcha must be filled out.使用Edge浏览器 Header Editor 插件安装 下载插件Header Editor 导入重定向脚本 点击扩展插件, 打开Header Editor插件,进行管理 点击导入输入下载链接进行下载或者导入本地json文件(二者任选其一…

QT 最近使用的项目配置文件

目录 1 QT 最近使用的项目配置文件所在路径 2 QtCreator.ini 1 QT 最近使用的项目配置文件所在路径 C:\Users\your username\AppData\Roaming\QtProject QtCreator.ini最好先备份一份 2 QtCreator.ini ProjectExplorer 下面的 RecentProjects\FileNames RecentProjects\…

ThreadPool-线程池使用及原理

1. 线程池使用方式 示例代码: // 一池N线程 Executors.newFixedThreadPool(int) // 一个任务一个任务执行,一池一线程 Executors.newSingleThreadExecutorO // 线程池根据需求创建线程,可扩容,遇强则强 Executors.newCachedThre…

C++:继承的介绍和深度解析

一、继承的概念和定义 1.什么是继承? 继承,顾名思义:就和现实生活中,孩子继承父母的东西有点类似。比如,你父亲的财产,你可以继承下来,你就可以使用父亲的钱。 官方一点的介绍: 继承…

JUC/多线程 模式(四)

一、同步模式之保护性暂停 即 Guarded Suspension ,用在一个线程等待另一个线程的执行结果 产生结果的线程和使用结果的线程是一一对应的,有多少个生产结果的线程就有多少个使用结果的线程。 要点 有一个结果需要从一个线程传递到另一个线程&#xff0…

JUC/多线程原理(三)

一、Monitor 原理 二、synchronized 原理 (一)、基础 synchronized 即使内部抛出异常也会释放锁 (二)、轻量级锁 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是 没有竞争 ),那么…

java分割回文串(力扣Leetcode131)

分割回文串 力扣原题链接 问题描述 给定一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。 示例 示例 1: 输入:s “aab” 输出:[[“a”,“a”,“b”],[“aa”,“b”]] 示例 2: 输…

【御控物联】 IOT异构数据JSON转化(场景案例一)

文章目录 前言技术资料 前言 随着物联网、大数据、智能制造技术的不断发展,越来越多的企业正在进行工厂的智能化转型升级。转型升级第一步往往是设备的智能化改造,助力设备数据快速上云,实现设备数据共享和场景互联。然而,在生产…

爬虫逆向实战(38)-某空气质量平台(反调试,AES,DES,MD5)

一、数据接口分析 主页地址:某空气质量平台 1、抓包 (1) 反调试 该网站对鼠标右击以及F12进行了监听并拦截 虽然该网站无法打开Chrome控制台,导致我们无法抓包,但是道高一尺魔高一丈。既然我们无法在打开该网站的时候打开Chrome控制台&…

高架学习笔记之UML图概要

目录 零、什么是UML图 一、类图 二、对象图 三、构件图 四、部署图 五、制品图 六、包图 七、组合结构图 八、用例图 九、序列图 十、通信图 十一、状态图 十二、活动图 十三、定时图 十四、交互概览图 零、什么是UML图 统一建模语言(Unified Modeli…

【Qt】:坐标

坐标 一.常用快捷键二.使用帮助文档三.Qt坐标体系1.理论2.代码 一.常用快捷键 注释:ctrl / • 运⾏:ctrl R • 编译:ctrl B • 字体缩放:ctrl ⿏标滑轮 • 查找:ctrl F • 整⾏移动:ctrl shift ⬆/…

protobuf学习笔记(二):结合grpc生成客户端和服务端

上一篇文章大概讲了如何将自定义的protobuf类型的message转换成相应的go文件,这次就结合grpc写一个比较认真的客户端和服务器端例子 一、项目结构 client存放rpc服务的客户端文件 server存放rpc服务的服务端文件 protobuf存放自定义的proto文件 grpc存放生成的g…

代码随想录训练营Day36:● 435. 无重叠区间 ● 763.划分字母区间 ● 56. 合并区间

435. 无重叠区间 题目链接 https://leetcode.cn/problems/non-overlapping-intervals/description/ 题目描述 思路 直接统计重叠区间的个数,就是需要删除的个数 public int eraseOverlapIntervals(int[][] intervals) {Arrays.sort(intervals,(a,b)-> Intege…

【电子取证篇】哈希校验值的变与不变

【电子取证篇】哈希校验值的变与不变 哈希值(散列值)是针对电子数据内容来计算的,内容变则哈希变;但计算对象的文件名、文件时间等属性改变不会影响散列值!!!—【蘇小沐】 (一&…

点点数据K参数加密逆向分析(RPC方案跟加密算法还原)

文章目录 1. 写在前面2. 接口分析3. 断点分析4. RPC调用5. 算法还原 【🏠作者主页】:吴秋霖 【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长…

【论文通读】UFO:A UI-Focused Agent for Windows OS Interaction

UFO:A UI-Focused Agent for Windows OS Interaction 前言AbstractMotivationMethodsExperimentConclusion 前言 Windows客户端第一个JARVIS,利用GPT4 Vision识别截图信息辅助智能体自动化执行操作,作为微软大肆宣传的一篇工作,其…

什么是齐纳二极管?齐纳二极管1SMB5944BT3G参数详解+应用方案

关于齐纳二极管基本知识: 齐纳二极管,又称稳压二极管。利用PN结的反向击穿状态,电流变化范围大,电压基本不变。制作了具有稳压功能的二极管。这种二极管是一个高电阻半导体器件,直到临界反向击穿电压。在这个临界击穿…