为遗留 Node.js 后端编写自动化测试

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
简介: Node.js作为后端框架,自 2009 年首次发布以来,已被越来越多的公司广泛采用。它的成功有以下几个原因:JavaScript 语言(又称 Web 语言)的应用,一个丰富的开源模块和工具的生态系统,以及它简单高效的原型 API。

Node.js作为后端框架,自 2009 年首次发布以来,已被越来越多的公司广泛采用。它的成功有以下几个原因:JavaScript 语言(又称 Web 语言)的应用,一个丰富的开源模块和工具的生态系统,以及它简单高效的原型 API。

不幸的是,简单是一把双刃剑。一个简单的 Node.js API,随着增长会变得越来越复杂,缺乏软件设计和最佳实践经验的开发人员可能很快就会被软件熵、偶然的复杂性或技术债务所淹没。

此外,JavaScript 语言的灵活性很容易被滥用,正常可用的原型在生产环境中跑着跑着就会很快变成不可维护的怪物。在使用 Node.js 启动一个项目时,很容易会忽视传统上与 Java 和 C#等 OOP 语言一起使用的最佳实践(例如SOLID原则),当然,这说不好会更好,还是会更坏。

当我帮助我的客户(大多数是刚起步的公司)改进他们的 Node.js 代码库时,以及在我编写的开源项目中,我感受到了软件熵的痛苦。例如,在维护 10 年前开始编写的 Node.js 应用程序 openwhyd.org 时,我面临着越来越多的挑战。我经常在客户的 Node.js 代码库中发现类似的挑战:正在增加的功能会破坏看似不相关的功能,bug 变得难以检测和修复,自动化测试编写起来很有挑战性,运行速度慢,而且会因为奇怪的原因失败……

让我们来探究一下为什么有些 Node.js 代码库比其他的更难测试。并探讨编写简单、健壮和快速检查业务逻辑的测试的几种技术。包括依赖注入(即 SOLID 的“D”),认可测试以及(剧透警告)没有模拟(mock)!

测试业务逻辑

举一个实际的例子,我们介绍一下 Openwhyd 中一个还没有被自动化测试覆盖到的特性:“热门曲目(Hot Tracks)”。

image.png

这个功能是一个在过去 7 天内 Openwhyd 用户最常发布、喜欢和播放的音乐排行榜。

它由三个用例组成:

显示曲目排行列表;

当一首歌曲被发布、转发、点赞和/或播放时,更新排名;

通过曲目排名的变化,显示每首歌曲的流行趋势(即上升、下降还是稳定)。

为了防止在这三个用例的愉快路径上出现回归,让我们将下列测试用例描述为行为驱动开发(BDD)场景:

image.png

让我们想象一下如何将第一个场景变成一个理想的自动化测试:

image.png

在这个阶段,这个测试不会通过,因为getHotTracks()需要一个数据库连接,我们的测试没有提供,并且storeTracks() 还没有实现。

从现在开始,通过测试将是我们的目标。为了更好地理解为什么“热门曲目”难以以这种方式进行测试,让我们来研究一下当前的实现。

为什么这个测试不能通过(当前)

目前,Openwhyd 的热门曲目特性由几个从models/tracks.js文件导出的函数组成:

  • getHotTracks()被 HotTracks API 控制器调用,在渲染之前获取排序的曲目列表;
  • updateByEid()在曲目被用户更新或删除时被调用,以更新其流行度得分;
  • snapshotTrackScores() 在每个星期天都调用,以便计算在下一周中显示的每个曲目的趋势。

让我们看看getHotTracks()函数是做什么的:

image.png

为这个函数编写单元测试很复杂,因为它的业务逻辑(例如,计算每个曲目的趋势)与一个数据查询交织在一起,该数据查询发送到一个全局的 MongoDB 连接(mongodb.js)。

这意味着,在当前的实现中,测试 Openwhyd 的热门曲目逻辑的唯一方法是:

通过发送 API 请求到一个连接到 MongoDB 服务器的正在运行的 Openwhyd 服务器,从而把这个系统作为一个黑盒来进行测试;

在初始化依赖的 MongoDB 数据库后,直接调用这些函数。

这两个解决方案都需要启动并在 MongoDB 数据库服务器上造数。这将使我们的测试实现起来很复杂,运行起来也很慢。

结论:业务逻辑与 I/O(例如数据库查询)耦合使编写测试变得困难,降低了它们的执行速度,并使这些测试变得脆弱。

模拟的问题

避免依赖 MongoDB 数据库运行测试的一种方法是使用Jest所谓的“mock”来模拟该数据库。(或称之为“桩”,正如 Martin Fowler 在《模拟不是桩》中给出的定义)

注入模拟要求测试运行程序将待测系统使用的依赖项(例如,我们服务器使用的数据库客户端)与一个假冒的版本热交换,以便自动化测试可以覆盖该依赖项的行为。

在我们的例子中,fetchRankedTracks()函数调用mongodb.tracks.find(),从 mongodb 模块导入。因此,我们的自动化测试可以设置一个假的内存数据库,将数据查询重定向到它,而不是真的去查询一个实际的 MongoDB 数据库:

image.png

这完全可行。

但是,如果测试中的特性多次调用同一个函数进行不同的查询,该怎么办?

image.png

在这种情况下,初始化它们的模拟和测试很快就会变得更大、更复杂,因而更难维护。

image.png

更重要的是,这样做意味着自动化测试依赖于独立于业务逻辑的实现细节。

两个原因:

mock 将与我们的数据模型的实现绑定在一起,也就是说,每当我们决定重构它时,我们都必须重写它们(例如重命名属性);

mock 会被绑定到被替换的依赖的接口上,也就是说,每当我们升级 mongodb 到一个新的版本,或者我们决定将数据库查询迁移到一个不同的 ORM 时,我们都必须重写它们。

这意味着即使业务逻辑没有改变,有时我们也必须更新我们的自动化测试!

在我们的例子中,如果我们决定在测试中模拟 mongodb 依赖,编写和更新测试将需要更多的工作。为了避免这种情况,开发人员可能会被劝着去升级依赖关系、更改数据模型,或者更甚:一开始就编写测试!

当然,我们宁愿节省一些时间去做更重要的事情,比如实现新功能!

提示:当依赖模拟来测试紧密耦合的代码时,即使业务逻辑没有改变,自动化测试也可能会失败。从长远来看,模拟数据库查询会使测试更不稳定,可读性更差。

依赖注入

根据前面的示例,模拟数据库查询不太可能是测试业务逻辑的可行的、长期的方法。

我们是否可以抽象业务逻辑和数据源 mongodb 之间的依赖关系,作为一种替代方法?

是的。我们可以通过让特性的调用者注入一种让业务逻辑获取所需数据的方法,来解耦特性及其底层的数据获取逻辑。

在实践中,我们不是从我们的模型中导入 mongodb,而是将该模型作为一个参数传递,以便调用者可以在运行时指定该数据源的任何实现。

下面是如何将 getHotTracks()函数转换为 TypeScript 中表达的类型:

image.png

这种方式:

在getHotTracks()调用时可以基于我们的应用程序的执行环境,注入fetchRankedTracks()和fetchCorrespondingPosts()的不同实现:基于 mongodb 的实现将被用于生产,而自定义的内存实现将针对每个自动化测试进行实例化;

我们不需要启动数据库服务器,也不需要运行测试来注入模拟,就可以测试模型的逻辑;

当数据库客户机的 API 变更时,自动化测试不需要更新。

结论:依赖注入有助于业务逻辑和数据持久层之间的解耦。我们可以重构紧耦合的代码,以使其更容易理解和维护,并为其编写健壮和快速的单元测试。

小心驶得万年船

在前一节中,我们了解了依赖注入如何帮助业务逻辑和数据持久层之间的解耦。

为了防止在重构当前实现时出现 bug,我们应该确保重构不会对特性的行为产生任何影响。

为了检测紧密耦合的代码中没有被自动化测试充分覆盖的行为变化,我们可以编写认可测试。认可测试预先收集曲目,在实现变更后再次执行检查这些曲目是否保持不变。它们是临时的,直到有可能为我们的业务逻辑编写更好的测试(例如单元测试)为止。

在我们的例子中:

  • 在输入(或触发器)方面:当 HTTP 请求被/hot和/api/post端点接收,由 Openwhyd 的 API 触发“热门曲目”特性;
  • 在输出(或曲目)方面:这些 HTTP 端点提供响应,并可能在 tracks 数据集合中插入和/或更新对象。

因此,我们应该能够通过发出 API 请求并观察结果响应中的变化和/或 tracks 数据集合的状态来检测功能回归。

image.png

image.png

请注意,这些测试可以按原样针对 Openwhyd 的 API 运行,因为它们只操作外部接口。因此,这些认可测试也可以作为灰盒测试或端到端 API 测试。

我们第一次运行这些测试时,这些测试运行程序将为每个测试断言生成包含传递给toMatchSnapshot()的数据的快照文件。在将这些文件提交到我们的版本控制系统(例如 git)之前,我们必须检查数据是否正确,是否足以作为参考。因此有了这个名字:"认可测试"。

注意:重要的是要阅读测试函数的实现,以发现这些测试必须覆盖的参数和特征。例如,getHotTracks()函数接受一个用于分页的 limit 和 skip 参数,并且它合并从 post 集合获取的额外的数据。确保相应地增加认可测试的覆盖范围,以检测该逻辑所有关键部分的回归。

问题:相同的逻辑,不同的曲目

提交快照并重新运行认可测试后,您可能会发现它们失败了!

image.png

Jest 告诉我们,每次运行时对象标识符和日期都不一样……

为了解决这个问题,我们在将结果传递给 Jest 的toMatchSnapshot()函数之前,用占位符替换动态值:

image.png

现在,我们已经为这些用例保留了预期输出的参考,可以安全地重构我们的代码并确保输出保持一致再次运行这些测试了。

为单元测试重构

现在,我们有了认可测试来警示我们“热点曲目”特性的行为是否发生了变化,我们可以安全地重构该特性的实现了。

为了减少我们即将开始的重构过程中的认知负荷,让我们从以下步骤开始:

删除所有死代码和/或注释掉的代码;

在异步函数调用上使用 await,而不是在 promise 上传递回调或调用.then();(这将大大简化编写测试和移动代码块的过程)

在依赖于数据库的遗留函数的名称后面添加上FromDb后缀,以便与我们即将引入的新函数有明显的区分。(例如,将getHotTracks()函数重命名为getHotTracksFromDb(),并将fetchRankedTracks()函数重命名为fetchRankedTracksFromDb())

根据我们的直觉开始重命名和移动代码块是很具风险的。这样做的风险在于,最终生成的代码很难测试……

让我们换成另一种方式:编写一个测试,清楚明确地检查特性的行为,然后重构代码,以便测试能够通过。测试驱动开发过程(TDD)将帮助我们想出一个新的设计,使该功能易于测试。

我们将要编写的测试是单元测试。因此,它们运行起来非常快,不需要启动数据库,也不需要 Openwhyd 的 API 服务器。为了实现这一点,我们将提取业务逻辑,这样就可以脱离底层基础设施独立进行测试。

另外,我们这次不打算使用快照。相反,让我们确切表达人类可读的特性应该如何运行的预期,类似于早期的 BDD 应用程序。

让我们从一个非常简单的问题开始:如果 Openwhyd 上只有一首曲目,它应该被列在热门曲目的首位。

image.png

到目前为止,这个测试是有效的,但它无法通过,因为没有定义getHotTracks()。让我们提供最简单的实现,仅仅是为了让测试通过。

image.png

现在测试通过了,TDD 方法的第三步表明我们应该清理和/或重构我们的代码,但到目前为止还没有太多要做的!因此,让我们通过编写第二个测试来开始第二个 TDD 迭代。

这个测试没有通过,因为getHotTracks()返回的是一个为了让第一个通过测试的硬编码的值。为了让这个函数在两个测试用例中都能工作,让我们提供输入数据作为参数。

image.png

image.png

现在,我们的两个单元测试通过了一个非常基本的实现,让我们尝试让getHotTracks()更接近它的实际实现(称为getHotTracksFromDb()),即当前在生产中使用的实现。

为了保持这些测试的纯粹性(即不产生任何副作用,因此不运行任何 I/O 操作的测试),它们调用的getHotTracks()函数必须不依赖于数据库客户端。

为了实现这一点,让我们应用依赖关系注入:用getTracksByDescendingScore()函数替换getHotTracks()的poststedtracks参数(类型:曲目的数组),该函数将提供对这些曲目的访问。这将允许getHotTracks()在需要数据时调用该函数。因此,我们将更多的控制权交给getHotTracks(),同时将如何实际获取数据的责任转交给调用者。

image.png

image.png

现在,我们已经让getHotTracks()的纯粹的实现更接近真实的实现,让我们从真实的实现调用它!

image.png

image.png

我们的单元测试和认可测试仍然可以工作,证明我们没有破坏任何东西!

现在,“热门曲目”模型将我们的纯粹的“热门曲目”特性逻辑称为“热门曲目”,我们可以在编写单元测试时,逐步将逻辑从第一个转移到第二个。

我们的下一步将是把来自于posts中的带有附加元数据的完整的tracks数据,从getHotTracksFromDb()移动到getHotTracks()。

我们从生产逻辑中观察到:

与tracks类似,posts是通过调用fetchPostsByPid()函数从数据库中获取的,所以我们将不得不再次对该函数应用依赖注入;

track和post集合之间的数据由eId和pId两个字段关联。

在转移该逻辑之前,基于这些观察,让我们将getHotTracks()的预期行为定义为一个新的单元测试。

image.png

image.png

为了让测试通过,我们将调用移动到fetchPostsByPid()以及它的后续逻辑,从getHotTracksFromDb()到getHotTracks()。

image.png

此时,我们将所有的数据操作逻辑转移到getHotTracks(),而getHotTracksFromDb()只包含必要的管道,以向它提供来自数据库的实际数据。

要让测试通过,我们只需要做最后一件事:将fetchPostsByPid()函数作为参数传递给getHotTracks()。对于我们的两个初始测试,fetchPostsByPid()可以返回一个空数组。

image.png

image.png

现在,我们已经成功地将业务逻辑从getHotTracksFromDb()提取到getHotTracks(),并使用单元测试覆盖了该纯粹的逻辑,我们可以安全地删除之前编写的防止该函数回归的认可测试:它会呈现排名的曲目。

我们可以遵循完全相同的过程完成剩下的两个用例:

  1. 基于 BDD 场景编写单元测试,
  2. 重构底层函数,让测试通过,
  3. 删除相应的认可测试。

结论

我们改进了代码库的可测试性和测试方法:

  • 研究了一个生产代码的例子,因为业务逻辑与数据库查询紧密耦合,所以测试起来很复杂;
  • 讨论了针对逻辑编写自动化测试时,依赖数据库(真实的或模拟的)的缺点;
  • 编写了认可测试,以检测重构逻辑时可能发生的任何功能回归;
  • 按照 TDD,使用依赖注入原则(又称“SOLID”中的“D”)逐步地重构逻辑;
  • 删除认可测试,支持我们在此过程中编写的纯粹的、人类可读的单元测试。

采用这些在面向对象编程语言(OOP)中被广泛接受和应用的模式和原则(例如 SOLID),可以帮助我们编写更好的测试,并使我们的代码库更易于维护,同时保持 JavaScript 和 TypeScript 环境的人类工程学。

我要感谢我的同事Julien Topçu (SHODO的技术教练),感谢他对概念的评审和改进,以及用于改进测试方法的示例。

作者简介

Adrien Joly,他自 2006 年以来一直是一名软件工程师,长驻于法国巴黎。他主要为初创公司工作。他关心的是,通过定期与技术和功能协作者进行协调和回顾,生产出既有用又易于编写的软件。在编写了他的第一个基于 node .js 的全栈 web 应用程序(openwhyd.org)十年之后,他仍然在生产环境中维护它,并使用它来实践遗留的代码重构技术。2020 年 3 月,Adrien Joly 加入咨询机构SHODO,与志趣相投的专业人士一起成长,并作为一名软件工匠实践自己的技能。你可以关注 Adrien 或者在 Twitter 上联系他:@adrienjoly。

原文链接:

Writing Automated Tests on a Legacy Node.js Back-end

译者简介

冬雨,小小技术宅一枚,现从事研发过程改进及质量改进方面的工作,关注研发、测试、软件工程、敏捷、DevOps、云计算、人工智能等领域,非常乐意将国外新鲜的 IT 资讯和深度技术文章翻译分享给大家,已翻译出版《深入敏捷测试》、《持续交付实战》。

了解更多软件开发与相关领域知识,点击访问 InfoQ 官网:https://www.infoq.cn/,获取更多精彩内容!

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。   相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
目录
相关文章
|
9天前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js: 打造高效后端服务
【10月更文挑战第39天】在数字化浪潮中,后端开发作为支撑现代Web应用的骨架,扮演着不可或缺的角色。Node.js,作为一种流行的服务器端JavaScript运行环境,因其非阻塞I/O和事件驱动的特性,被广泛应用于构建轻量且高效的后端服务。本文旨在通过浅显易懂的语言,结合生动的比喻和实际代码案例,带领读者深入理解Node.js的核心概念、架构设计及其在后端开发中的应用,进而掌握如何使用Node.js搭建稳定、可扩展的后端服务。无论你是初探后端开发的新手,还是寻求进阶的开发者,这篇文章都将为你提供有价值的指导和启示。
|
6天前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js:从零开始构建后端服务
【10月更文挑战第42天】在数字时代的浪潮中,掌握一门后端技术对于开发者来说至关重要。Node.js,作为一种基于Chrome V8引擎的JavaScript运行环境,允许开发者使用JavaScript编写服务器端代码,极大地拓宽了前端开发者的技能边界。本文将从Node.js的基础概念讲起,逐步引导读者理解其事件驱动、非阻塞I/O模型的核心原理,并指导如何在实战中应用这些知识构建高效、可扩展的后端服务。通过深入浅出的方式,我们将一起探索Node.js的魅力和潜力,解锁更多可能。
|
2天前
|
Web App开发 JavaScript 前端开发
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念,包括事件驱动、单线程模型和模块系统;探讨其安装配置、核心模块使用、实战应用如搭建 Web 服务器、文件操作及实时通信;分析项目结构与开发流程,讨论其优势与挑战,并通过案例展示 Node.js 在实际项目中的应用,旨在帮助开发者更好地掌握这一强大工具。
13 1
|
14天前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js后端框架
【10月更文挑战第34天】在数字化时代,后端开发如同一座桥梁,连接着用户界面与数据处理的两端。本文将通过Node.js这一轻量级、高效的平台,带领读者领略后端框架的魅力。我们将从基础概念出发,逐步深入到实战应用,最后探讨如何通过代码示例来巩固学习成果,使读者能够在理论与实践之间架起自己的桥梁。
|
5天前
|
JavaScript
使用node.js搭建一个express后端服务器
Express 是 Node.js 的一个库,用于搭建后端服务器。本文将指导你从零开始构建一个简易的 Express 服务器,包括项目初始化、代码编写、服务启动与项目结构优化。通过创建 handler 和 router 文件夹分离路由和处理逻辑,使项目更清晰易维护。最后,通过 Postman 测试确保服务正常运行。
|
12天前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js后端开发
【10月更文挑战第36天】本文将引导您探索Node.js的世界,通过实际案例揭示其背后的原理和实践方法。从基础的安装到高级的异步处理,我们将一起构建一个简单的后端服务,并讨论如何优化性能。无论您是新手还是有经验的开发者,这篇文章都将为您提供新的视角和深入的理解。
|
15天前
|
Web App开发 JavaScript 前端开发
探索后端开发:Node.js与Express的完美结合
【10月更文挑战第33天】本文将带领读者深入了解Node.js和Express的强强联手,通过实际案例揭示它们如何简化后端开发流程,提升应用性能。我们将一起探索这两个技术的核心概念、优势以及它们如何共同作用于现代Web开发中。准备好,让我们一起开启这场技术之旅!
31 0
|
15天前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实践
【10月更文挑战第33天】在数字化时代的浪潮中,后端服务的效率和可靠性成为企业竞争的关键。本文将深入探讨如何利用Node.js和Express框架构建高效且易于维护的后端服务。通过实践案例和代码示例,我们将揭示这一组合如何简化开发流程、优化性能,并提升用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
1月前
|
机器学习/深度学习 人工智能 运维
构建高效运维体系:从自动化到智能化的演进
本文探讨了如何通过自动化和智能化手段,提升IT运维效率与质量。首先介绍了自动化在简化操作、减少错误中的作用;然后阐述了智能化技术如AI在预测故障、优化资源中的应用;最后讨论了如何构建一个既自动化又智能的运维体系,以实现高效、稳定和安全的IT环境。
67 4
|
1月前
|
运维 Linux Apache
,自动化运维成为现代IT基础设施的关键部分。Puppet是一款强大的自动化运维工具
【10月更文挑战第7天】随着云计算和容器化技术的发展,自动化运维成为现代IT基础设施的关键部分。Puppet是一款强大的自动化运维工具,通过定义资源状态和关系,确保系统始终处于期望配置状态。本文介绍Puppet的基本概念、安装配置及使用示例,帮助读者快速掌握Puppet,实现高效自动化运维。
54 4