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

news2024/9/20 15:28:13

​在《你在测试金字塔的哪一层(上)》中介绍了自动化测试的重要性以及测试金字塔。测试金字塔分为单元测试、服务测试、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;
 
    @Autowired
    public 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;
 
    @Mock
    private PersonRepository personRepo;
 
    @Before
    public void setUp() throws Exception {
        initMocks(this);
        subject = new ExampleController(personRepo);
    }
 
    @Test
    public 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!"));
    }
 
    @Test
    public 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 {
    @Autowired
    private PersonRepository subject;
 
    @After
    public void tearDown() throws Exception {
        subject.deleteAll();
    }
 
    @Test
    public 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.coloradmin.cn/o/1544632.html

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

相关文章

蓝桥杯练习07小兔子爬楼梯

小兔子爬楼梯 介绍 小兔子想去月球上旅行,假设小兔子拥有一个阶梯子,当你爬完层就可以到达月球,小兔子每次可以跳1或者2个台阶,小兔子有多少种跳法可以到达月球呢? 给定n是一个正整数,代表梯子的阶数&…

apisix创建https

总结了下apisix 使用https 的问题和方法 1、apisix 默认https 端口是9443 2、apisix 需要上传证书后才可以使用https 否二curl测试会报错 SSL routines:CONNECT_CR_SRVR_HELLO 3、apisix 上传证书方法 我是使用的自签名证书,注意自签名证书的Common Name 要写你…

【教程】高效数据加密混淆方法及实现简介

背景 在需要对数据进行传输或者表达时,通常要求数据加密的安全级别不高,但希望加解密时间复杂度尽可能低。这时使用传统的对称加密(如3DES、AES)或非对称加密(如RSA、ECC)显然不太适合。因为加密的安全级别…

基于深度学习的海洋鱼类识别算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 MATLAB2022a 3.部分核心程序 ............................................................ % 对测试集进行分类预测 [Pr…

深入理解 TreeMap

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好…

厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件开发之功能结构篇

厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件开发之功能结构篇 好几年前,应朋友之邀,为其工厂的厨余垃圾处理设备研发一套用于对现场的生产及维护进行远程查看、管理和质量监控的厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件系统。 因为…

STM32之HAL开发——Keil调试工具介绍

Debug介绍 在Keil工具中有许多常用的小工具,下面将会依次为大家介绍每个工具的用途。 命令行窗口 在窗口内可以输入一些指令,来进行断点设置以及删除,一般不常用 反汇编窗口 可以查看当前C代码的汇编指令 标志窗口 寄存器窗口 可以用来查看C…

【Linux】信号量与信号

目录 先导知识 信号量 信号 信号概念及产生信号的一般方式 进程递达、阻塞和捕捉 信号集操作函数 信号的捕捉 可重入函数 先导知识 信号量与信号没有任何关系,它们是两个完全不同的概念! 操作系统的本质,就是一个死循环;…

Cookie在网络爬虫中的重要作用

在互联网的海量数据中,网络爬虫如同一只勤劳的小蜜蜂,不断采集着网页上的信息。而在这过程中,Cookie扮演了不可或缺的角色,它就像是爬虫手中的“通行证”,帮助其顺利获取所需数据。本文将深入探讨Cookie在网络爬虫中的…

LLM2LLM: Boosting LLMs with Novel Iterative Data Enhancement

LLM2LLM: Boosting LLMs with Novel Iterative Data Enhancement 相关链接:arXiv GitHub 关键字:LLM、Data Augmentation、Fine-tuning、NLP、Low-data Regime 摘要 预训练的大型语言模型(LLMs)目前是解决绝大多数自然语言处理任…

axios+springboot上传图片到本地(vue)

结果&#xff1a; 前端文件&#xff1a; <template> <div> <input type"file" id"file" ref"file" v-on:change"handleFileUpload()"/> <button click"submitFile">上传</button> </div&g…

centos7 的redis的安装

文章目录 查看本机redis⾸先安装 scl 源, 再安装 redis 基本配置启动redis停止redis 查看本机redis ⾸先安装 scl 源, 再安装 redis 安装scl源 yum install centos-release-scl-rh安装redis5 yum install rh-redis5-redis安装成功 基本配置 修改etc/redis/redis.conf 文件…

javaWeb网上订餐管理系统

一、简介 在当今社会&#xff0c;随着互联网的普及&#xff0c;网上订餐已经成为了人们生活中不可或缺的一部分。为了方便用户点餐&#xff0c;同时也方便商家管理订单&#xff0c;我设计了一个基于JavaWeb的网上订餐管理系统。该系统分为前台和后台两部分&#xff0c;前台包括…

解决多线程场景下ThreadLocal的变量传递问题

问题描述&#xff1a; ThreadLocal可以用于存储线程独享的变量。可以方便的存储上下文信息&#xff0c;提升代码的简洁性。 然而&#xff0c;ThreadLocal的一个不足之处在于&#xff0c;它不支持在线程嵌套过程中自动地将数据从父线程传递到子线程。这意味着&#xff0c;即使…

面试笔记——框架篇Spring系列(Spring、SpringMVC、SpringBoot)

Spring 线程安全 singleton : bean在每个Spring IOC容器中只有一个实例。 prototype&#xff1a;一个bean的定义可以有多个实例。 问题一&#xff1a; Spring中的单例bean是否是线程安全的&#xff1f; ControllerRequestMapping("/user")public class UserContro…

Springboot+vue的旅游信息推荐系统设计与实现+数据库+论文+数据库表结构文档+免费远程调试

项目介绍: Springbootvue的旅游信息推荐系统设计与实现。Javaee项目&#xff0c;springboot vue前后端分离项目 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring SpringBoot Mybati…

为何ChatGPT日耗电超50万度?

看新闻说&#xff0c;ChatGPT每天的耗电量是50万度&#xff0c;国内每个家庭日均的耗电量不到10度&#xff0c;ChatGPT耗电相当于国内5万个家庭用量。 网上流传&#xff0c;英伟达创始人黄仁勋说&#xff1a;“AI的尽头是光伏和储能”&#xff0c;大佬的眼光就是毒辣&#xff…

【云能耗管理系统在某大型商场的应用】安科瑞Acrel-EIOT能源物联网平台方案

摘要&#xff1a;依据对上海市某大型商场现场考察的结果&#xff0c;提出通过建设云能耗管理系统的方案来改善商场能耗的管理现状。首先充分搜集建筑信息和设备运行工况&#xff0c;合理设计系统实施方案&#xff0c;解决现场数据采集和传输障碍&#xff0c;完成云能耗管理系统…

Python 全栈体系【四阶】(十九)

第五章 深度学习 一、基本理论 4. 神经网络的改进 4.3 循环神经网络 4.3.1 标准 CNN 模型的不足 假设数据之间是独立的。标准 CNN 假设数据之间是独立的&#xff0c;所以在处理前后依赖、序列问题&#xff08;如语音、文本、视频&#xff09;时就显得力不从心。这一类数据…

解决 cv2.imread读取带中文路径图片问题

http://t.csdnimg.cn/i8CXn 1.问题&#xff1a; # 中草药数据集样本可视化展示 import cv2 import matplotlib.pyplot as plt %matplotlib inline plt.title("heshouwu") plt.imshow(cv2.imread(r"D:\home\aistudio\data1\archive\train\何首乌\heshouwu_0001.…