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

​在《你在测试金字塔的哪一层(上)》中介绍了自动化测试的重要性以及测试金字塔。测试金字塔分为单元测试、服务测试、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》

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

猪瘟病毒筛查系统的工作原理

TH-P160S猪瘟病毒筛查系统是一种专门用于检测猪瘟病毒的设备或技术组合,其核心目的是确保生猪养殖、产品流通等环节的安全,防止猪瘟病毒的扩散和传播。猪瘟,又称为“烂肠瘟”,是由黄病毒科瘟毒病属的猪瘟病毒引起猪的一种急性、发…

如何使用PHP和RabbitMQ实现延迟队列(方式二)?

前言 前几天写了一篇关于PHP和RabbitMQ如何通过插件实现延迟队列的功能。 今天写另外一篇不需要插件的方式,使用RabbitMQ的死信队列(Dead-Letter-Exchanges, DLX)和消息TTL(Time-To-Live)。 这种方法涉及到设置消息…

3款免费甘特图制作工具的比较和选择指南

GanntProject GanttProject https://www.ganttproject.biz/ 是一款项目管理和调度应用,适用于 Windows、macOS 和 Linux。它易于使用,无需任何设置,适用于个人用户和小型团队。该应用提供任务层次结构和依存关系、里程碑、基准行、Gantt 图表…

Knative 助力 XTransfer 加速应用云原生 Serverless 化

作者:元毅 公司介绍 XTransfer 是一站式外贸企业跨境金融和风控服务公司,致力于帮助中小微企业大幅降低全球展业的门槛和成本,提升全球竞争力。公司连续7年专注 B2B 外贸金融服务,已成为中国 B2B 外贸金融第一平台,目…

20240325,结构嵌套,联合,全局变量,编译预处理和宏,声明

二&#xff0c;结构 2.3 结构中的结构 2.3.1 结构数组 #include<stdio.h>//下一秒 struct time{int hour;int min;int sed; }; struct time timeupdate(struct time now); int main(){struct time testTime[5]{{11,59,59},{12,0,0},{1,29,59},{23,59,59},{19,12,27}…

数据结构 之 队列习题 力扣oj(附加思路版)

优先级队列 #include<queue> --队列 和 优先级队列的头文件 优先级队列&#xff1a; 堆结构 最大堆 和 最小堆 相关函数&#xff1a; front() 获取第一个元素 back() 获取最后一个元素 push() 放入元素 pop() 弹出第一个元素 size() 计算队列中元素…

Maven学习记录

一、简介 1. Maven&#xff1a; 基于 Java 平台的项目管理和整合工具&#xff0c;将项目的开发和管理过程抽象成一个项目对象模型&#xff08;POM&#xff09;。开发人员只需要做一些简单的配置&#xff0c;Maven 就可以自动完成项目的编译、测试、打包、发布以及部署等工作。…

把学浪视频保存到电脑方法

为了可以更好的学习很多用户都会想要将学浪的视频下载下来,但是学浪视频官方却没有提供下载方法,为了将学浪视频下载下来我研究了一段时间,总算有突破,找到了下载方法 文章中所用到的工具就在下面,有需要的自己取一下 链接&#xff1a;https://pan.baidu.com/s/1y7vcqILToULr…

go的for循环应该这么用

目录 目录 一&#xff1a;介绍 1: for流程控制 2&#xff1a;for-range流程控制 二&#xff1a;实例展示 1&#xff1a;//按照一定次数循环 2&#xff1a;//无限循环 3: //循环遍历整数、各种容器和通道 4&#xff1a;遍历通道 5&#xff1a;//指针数组循环 6&…

git笔记之撤销、回退、reset方面的笔记

git笔记之撤销、回退、reset方面的笔记 code review! 文章目录 git笔记之撤销、回退、reset方面的笔记1.git 已经commit了&#xff0c;还没push&#xff0c;如何撤销到初始状态git reset --soft HEAD~1git reset HEAD~1&#xff08;等同于 git reset --mixed HEAD~1&#xff0…

机器学习OpenNLP

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl OpenNLP概述 OpenNLP是一个基于机器学习的自然语言处理开发工具包&#xff0c;它是Apache软件基金会的一个开源项目。OpenNLP支持多种自然语言处理任务&#xff0c;如分词、…

计算机网络:现代通信的基石

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

如何忽略Chrome最小字号的限制

通过控制台调整字体大小时&#xff0c;可以发现即便设置了小于12px的字号&#xff0c;也并不会变小&#xff0c;这是因为Chrome默认最小字号为12px。 在Chrome设置中的外观选项卡中可以发现&#xff0c;默认字体是16px。将最小字号改为0&#xff0c;就能随意设置小于12px的字号…

面向对象【枚举类】

文章目录 枚举类定义枚举类enum 方式定义的要求和特点 enum 中常用方法实现接口的枚举类 枚举类 枚举类是一种特殊的类&#xff0c;它用于定义一组固定数量的常量。枚举类在实际开发中非常有用&#xff0c;因为它们可以增加代码的可读性和可维护性。本文将介绍Java枚举类的定义…

[网鼎杯2018]Unfinish 两种方法 -----不会编程的崽

网鼎杯太喜欢搞二次注入了吧。上次是无列名盲注&#xff0c;这次又是二次注入盲注。。。不知道方法还是挺难的。哎&#xff0c;网鼎嘛&#xff0c;能理解透彻就很强了。能自己做出来那可太nb了。 又是熟悉的登录框。不知道这是第几次看见网鼎杯的登录框了。后台扫描一下&#x…

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

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

西安石油大学校赛培训(1)数学模型简介 初等模型

数学建模竞赛 什么是数学建模竞赛?数学竞赛给人的印象是高深莫测的数学难题,和一个人、一支笔、一张纸&#xff0c;关在屋子里的冥思苦想&#xff0c;它训练严密的逻辑推理和准确的计算能力&#xff0c;而数学建模竞赛从内容到形式与此都有明显的不同。 数学建模竞赛的题目由日…

高防服务器、高防IP、高防CDN的工作原理是什么

高防IP高防CDN我们先科普一下是什么是高防。“高防”&#xff0c;顾名思义&#xff0c;就犹如网络上加了类似像盾牌一样很高的防御&#xff0c;主要是指IDC领域的IDC机房或者线路有防御DDOS能力。 高防服务器主要是比普通服务器多了防御服务&#xff0c;一般都是在机房出口架设…

学点儿Java_Day10_集合框架(List、Set、HashMap)

1 简介 ArrayList: 有序(放进去顺序和拿出来顺序一致)&#xff0c;可重复 HashSet: 无序(放进去顺序和拿出来顺序不一定一致)&#xff0c;不可重复 Testpublic void test1() {String[] array new String[3];//List: 有序 可重复//有序: 放入顺序 与 拿出顺序一致&#xff0c;…

Github多账号共存

在开发阶段&#xff0c;如果同时拥有多个开源代码托管平台的账户&#xff0c;在代码的管理上非常麻烦。那么&#xff0c;如果同一台机器上需要配置多个账户&#xff0c;怎样才能确保不冲突&#xff0c;不同账户独立下载独立提交呢&#xff1f; 我们以两个github账号进行演示 …