微服务架构是指将应用程序拆分为一系列较小、且直接用于解决具体问题的组件的实践方案。以此为基础,架构中的每一个组件都将通过各类常规协议(例如 HTTP 或者更轻量化的 TCP)相互通信。
说到这里,大家可能会好奇,对于微服务架构来说,测试真的很重要吗?
答案当然是重要!测试的重要性是体现在多方面的,不过比较重要的是以下几点:
节约金钱与时间
更安全
强化生产质量(减少 bug 与错误数量)
提升客户满意度
最重要的是,夜里能睡得更安稳
随时出 Bug、动不动就宕机的应用程序,没人会喜欢,而且往往这种应用程序的安全漏洞很多,如果黑客想从中窃取凭证或者抢劫资金,简直易如反掌。如果我们想要开发一款具备一定复杂性的应用程序,那么测试是一定需要的。
使用什么测试方法?
目前软件测试的种类比较多,大致可以分为功能测试和非功能测试两大类。其中功能测试类包括单元测试、集成测试、通烟测试、回归测试、健全测试、Beta/ 验收测试和端到端(e2e)测试,而非功能测试则包括了性能测试、负载测试、压力测试、安全测试、合规测试和可用性测试。
一般来说,应用程序的复杂度越高,需要使用的测试类型也就越多。不过,有几个测试是所有应用程序都不可或缺的:
单元测试
集成测试
E2E 测试、回归测试与安全测试相结合
整体流程应该是,先编写程序来检查应用中各个层面是否在按照预期设计运作,若应用已经上线,那么就需要进一步编写测试来检查代码更新是否会对原有功能造成破坏。如果是微服务架构,那么除了以上的基础测试之外,可能还需要编写专门的测试,例如负载测试,用于检查系统在正常与预期峰值负载条件下的运行状况。
少说话,多编码
接下来,我们一起探讨一下如何在微服务架构当中实现上述基础软件测试类型。微服务架构使用 TCP 协议实现组件间的通信,并利用 Nest Framework 以 Node.JS 编写而成。
很多人可能不太了解 NestJS,我们先简单介绍一下,官方 GitHub repo 是这样描述 Nest 的:
Nest 是一款框架,用于构建高效且可扩展的 Node.js 服务器端应用程序。它利用现代 JavaScript 的特性,由 TypeScript 构建而成(保留纯 JavaScript 兼容性),同时结合有 OOP(面向对象编程)、FP(函数编程)以及 FRP(函数响应式编程)的元素。从底层来看,Nest 不仅能够使用 Express,同时也兼容其他多种库,包括 Fastify,旨在轻松使用大量现有第三方插件。”
在示例中,我们将使用一个简单的模块 name:user 配合一个简单函数 createUser,在数据库内创建一个新用户。
该模块的文件夹结构如下所示:
我们设了一个监听 create_user 消息的控制器。在利用 ValidationPipe 进行验证之后,它会在服务之内调用一个具有相同名称的函数。
在服务之内,我们会对用户密码进行哈希处理。接下来,使用 TypeORM 将新用户保存在数据库内。
对这个模块,我们使用 TypeORM 作为链接至表 User 的 ORM;同时,利用名为 UtilsModule 的另一个模块实现某些辅助功能:
单元测试
所谓单元,是指应用程序当中的最小可测试部分,例如函数、类或者过程。单元测试则代表一种软件测试方法,旨在测试源代码中的各个单元,以确定其是否符合开发阶段的预期设计。
编写单元测试,是为了保证不同代码形式(函数、类等)的每个简单实现,均符合设计、要求并能够按预期运行。
单元测试的目标,在于隔离程序中的各个部分,并测试这些部分是否正常工作。
换言之,与当前测试单元无关的其他代码部分,则以模拟形式存在,仅作为运行环境使用。
在我们的示例中,需要测试的单元自然就是之前提到的 createUser 了。这意味着我们首先得把它跟其他组件隔离开来。因此,第一步就是模拟 user repository 类,这个类代表着数据库使用 TypeORM 时的链接。
如果分析服务中的 createUser 函数,就会发现它的作用只是对密码进行哈希处理,而后将 User 对象保存在数据库内。以此为基础,我们编写出以下测试套件:
首先,我们编写一个 beforeAll 函数来创建测试模块。接下来,使用模拟类替换原始 repository,该模拟类将仅返回需要保存在数据库中的对象。
在这个函数中,我们还得考虑这样一种极端要求:
使用特定属性(邮件、密码等)创建一个新用户对象,请确保密码经过哈希处理
这里,我们会模拟 save() 函数,因为它来自 TypeORM、不属于测试中的对象单元范畴,使用简单函数将其覆盖以返回我们传递的对象。
到这里,我们的工作就很简单了:检查在发送对象过程中使用的邮件属性与哈希密码是否正确。
集成测试
集成测试属于另一种软件测试方法,用于验证源代码单元内的组成功能是否正常。
单元测试的目标是保证代码符合其设计与功能要求,同时能够按照预期方式运行。集成测试则更进一步,将不同模块融合在一起,并测试它们是否能够正确交互。
在本示例中,我们将 UserModule 与 TypeORM 模块(存在依赖关系)结合起来,检查新用户是否被正确保存在数据库内。
这一次,我们仍然需要使用之前提到的函数,只是具体测试流程有所区别:
这一次,beforeAll 函数不再模拟 userRepository,而直接使用原始库;此外,我们还添加 databaseModule 以创建指向数据库的连接。
与此同时,由于我们现在使用的是真实数据库,因此必须编写对应函数调整数据库以完成测试。
在测试之前与之后,我们需要清空数据库,保证不存在任何干扰内容。
另外,我们还需要手动关闭指向数据库的连接,这样才能保证测试完成后所有处理程序都被正确关闭。
通过单元测试,我们已经检查了函数能否正常工作。因此,这里可以直接测试该函数能够与 TypeORM 的 save() 方法相结合,进而将新用户对象存储在数据库内。
我们编写了名为 getOneUserFromDb 的辅助函数,它的作用顾名思义——从数据库内获取一个用户。接下来,检查邮件与 accountConfimed 属性是否正确(后者在实体类内应默认设置为 false)。
端到端测试
端到端测试是一种软件测试方法,旨在持续跟踪应用程序的整个运行流程是否与设计思路一致。
这类测试的目标,在于确保应用程序能否在真实场景下按预期方式运行。
到目前为止,我们已经测试了用户密码是否经过正确的哈希处理,以及密码及邮件是否被保存在数据库内。
现在,我们需要通过请求测试验证流程。我们的控制器内包含一条验证管道,通过测试传入的有效负载检查对象是否与 CreateUserDto 相匹配。
下面来看测试过程:
在这里,我们希望通过测试观察系统在创建用户之后,是否会发送完整对象或者以错误格式发送属性。
这就是我们在某些极端情况下,使用三种基础软件测试方法得出的示例结果。
手动测试与自动测试
说到这里,我们的测试过程一直以手动方式编写——因为示例规模不大,所以过程非常顺利。但如果代码量庞大,那么测试的复杂度与工作量会急剧增加。
例如,如果需要测试身份验证系统,大家就必须复制真实用户的完整行为。另外,在测试环境的构建阶段还需要模拟请求与响应部分,包括 cookie 及其他内容。很明显,测试套件越复杂,运行需要的时间就越长。
幸运的是,自动化工具已经成为当前测试工作中的有力武器。这类工具包含多种内置功能,允许用户模拟整个测试环境,轻松搞定手动方式几乎无法实现的测试流程。
大家还可以走得更远,在应用程序中用上 API 自动测试工具。这些工具带有多种附加选项,能够高效生成负载测试、回归测试与实际运行状况等数据报告。
另外,它们还拥有良好的 UI 设计,进一步降低测试编写难度。
总结
要让软件真正为生产环境做好准备,测试绝对是不可或缺的一环。而随着应用程序复杂性的持续提升,测试工作很可能成为开发团队的最大瓶颈。
在这种情况下,请确保按照具体类型将测试套件区分开来,正如我们在示例中的做法。想要测试哪类功能,就使用与之对应的套件,保证事半功倍。
如果手动套件不足以满足用例要求,或者您发现测试太难且编写耗时太长,那么不妨选择自动化工具及平台。目前这类方案已经相当成熟,绝对能够成为各位日常工作中的好帮手。
原文链接:
https://www.freecodecamp.org/news/testing-microservices-are-they-production-ready-2/
评论 1 条评论