查看原文
人权观察

【第2734期】JavaScript & Node.js 的测试最佳实践 - 第二章:后端测试

goldbergyoni 前端早读课 2022-09-24

前言

你的工作中有写测试么?今日前端早读课文章由 @goldbergyoni 分享。

正文从这开始~~

【第2732期】JavaScript & Node.js 的测试最佳实践 - 第一章: 测试剖析

第二章:后端测试

⚪ ️2.1 丰富您的测试组合:不局限于单元测试和测试金字塔

✅ 建议:测试金字塔,虽然已经有超过 10 年的历史了,但是它仍是一个很好的相关模型,它提出了三种测试类型,并且影响了大多数开发人员的测试策略。与此同时,大量闪亮的新测试技术出现了,并隐藏在测试金字塔的阴影下。考虑到近 10 年来我们所看到的所有巨变 (微服务、云、无服务器),这个非常老的模型是否仍能适用于所有类型的应用?测试界不应该考虑欢迎新的测试技术吗?

请不要误解,在 2019 年,测试金字塔、TDD、单测仍然是强大的技术,且对于大多数应用仍是最佳选择。但是像其他模型一样,尽管它有用,但是一定会在某些时候出问题。例如,我们有一个 IOT 应用,将许多事件注入一个 Kafka/RabbitMQ 这样的消息总线中,然后这些事件流入一些数据仓库并被分析 UI 查询。我们真的需要花费 50% 的测试预算去为这个几乎没有逻辑的集成中心化的应用写单测吗?随着应用类型 (机器人、密码、Alexa-skills) 的多样性增长,测试金字塔可能将不再是某些场景的最佳选择了。

是时候丰富你的测试组合并了解更多的测试类型了(下一节会给你一些小建议),这些类似于测试金字塔的思维模型与你所面临的现实问题更匹配(' 嘿,我们的 API 挂了,试试消费者驱动的合同测试!'),让您的测试多样化,比如建立基于风险分析的检查模型 —— 评估可能出现问题的位置,并提供一些预防措施以减轻这些潜在风险。

需要注意的是:软件世界中的 TDD 模型面临两个极端的态度,一些人鼓吹到处使用它,另一些人则认为它是魔鬼。每个说绝对的人都是错的 :]

❌ 否则:你将错过一些超高投入产出比的工具,比如 Fuzz、lint、mutation 这些工具只需 10 分钟配置就能贡献价值。

👏 正例: Cindy Sridharan 在她的文章 “测试微服务 —— 理智的方式” 中提出了一个丰富的测试组合


☺️Example: YouTube: “Beyond Unit Tests: 5 Shiny Node.JS Test Types (2018)” (Yoni Goldberg)

⚪ 2.2 组件化测试可能是最有效的利器

✅ 建议:应用的每个单元测试仅能覆盖应用的一小部分,覆盖全部会非常麻烦,而端到端测试可以很轻松地覆盖大量区域,但是比较脆弱而且很慢。何不找一个平衡点:写一些比单测大,但是比端到端测试小的测试。组件测试是测试世界的一颗遗珠 —— 它找到了两个模式的最佳平衡点:不错的性能和使用 TDD 模式的可能性 + 真实且强大的覆盖率。

组件测试关注于微服务 “单元”,他们反对 API,不 mock 任何属于微服务本身的东西(比如:真实的 DB,甚至是该 DB 的内存版本)但是 stub 所有外部的东西比如调用其他微服务。这么做,我们测试我们部署的部分,由外而内地覆盖应用,节省大量时间。

❌ 否则:你可能花了好几天写单测,却发现仅得到了 20% 的系统覆盖率。

👏 正例:使用 Supertest 测试 Express API (快速、覆盖很多层)

⚪ 2.3 保证新的 release 不会破坏 API 的使用

✅ 建议:你的微服务有很多的客户,而你为了兼容性运行着该服务的很多版本(keeping everyone happy)。当你改了某个字段后 “砰!”,依赖该字段的几个重要的客户炸锅了。服务端满足所有客户的期望是非常难的 —— 另一方面,客户无法发起测试,因为服务端控制着 release。Consumer-driven contracts and the framework PACT 诞生了,它以一种破坏性的方式规范了这一流程 —— 不再由服务端定义测试计划,而是客户端决定服务端的测试!PACT 可以记录客户端的期望 ——“中间人(Broker)”,并放置到共享空间,服务端可以 pull 下来这写期望并利用 PACT 的库在所有的版本中检测是否有被破坏的契约 —— 有客户端的期望没有被满足。通过这种方式,所有客户端 - 服务端不匹配的 API 将会在 构建 / CI 阶段被 catch 到,从而减少你大量的烦恼。

❌ 否则:所有的变更将带来繁琐的手动测试,导致开发者惧怕发布。

👏 正例:

⚪ 2.4 单独测试你的中间件

✅ 建议:许多人拒绝测试中间件,是因为它们仅占据系统的一小部分而且依赖真实的 Express server。这两个原因都不正确 —— 中间件虽然小,但是影响全部或者大部分请求,而且可以被简单地作为纯函数测试(参数为 {req,res} JS 对象)。测试中间件函数,你仅需调用它,并且 spy (比如使用 Sinon) {req,res} 的交互以保证函数执行了正确的行为。node-mock-http 库更进一步:它还监听了 {req,res} 对象的行为。例如,它可以断言 res 对象上的 http 状态是否符合预期。(看下面的例子)

❌ 否则: Express 中间件上的一个 bug === 所有或者大部分请求的 bug

👏正例:隔离地测试中间件,不发出网络调用或唤醒整个 Express 机器

//the middleware we want to test
const unitUnderTest = require('./middleware')
const httpMocks = require('node-mocks-http');
//Jest syntax, equivelant to describe() & it() in Mocha
test('A request without authentication header, should return http status 403', () => {
const request = httpMocks.createRequest({
method: 'GET',
url: '/user/42',
headers: {
authentication: ''
}
});
const response = httpMocks.createResponse();
unitUnderTest(request, response);
expect(response.statusCode).toBe(403);
});
⚪ 2.5 使用静态分析工具度量并指导重构

✅ 建议:使用静态度量工具可以帮助你客观地提升代码质量并使其可维护。你可以将静态分析工具放在你的 CI 中。除了普通 linting 外,它的主要卖点是结合多文件的上下文来检查质量(例如:发现重复定义)、执行高级分析(例如:代码复杂度)以及跟踪 code issue 的历史和进度。有两个工具供你使用:Sonarqube (2,600+ stars) and Code Climate (1,500+ stars)

贡献:: Keith Holliday

❌ 否则:由于代码质量差,再新的库和 feature 也无法拯救你的 bug 和性能。

👏 正例: CodeClimate —— 一个用于发现复杂方法的商业工具

⚪ 2.6 你是否准备好迎接 Node 相关的噪声

✅ 建议:怪异的是,大部分软件测试仅关注逻辑和数据,但是最糟糕(而且很难减轻)的往往是基础设施问题。例如,你测试过当你的进程存储过载、服务器 / 进程挂掉时的表现吗?或者你的监控系统会检测到 API 减慢 50% 了吗?为了测试及减轻类似问题,Netflix 设立了 噪声工程。它的目的是为我们的系统在故障问题下的健壮性提供意识、框架及工具。比如,著名的工具之一 噪声猴子,随机地杀掉服务以保证我们的服务仍服务于用户,而不是仅依赖一个单独的服务器(Kubernetes 也有一个版本 kube-monkey 用于杀掉 pods)。这些工具都是作用于服务器 / 平台层面,但如果你想测试及生产纯粹的 Node 噪声比如检查你的 Node 进程如何处理未知错误、未知的 promise rejection、v8 内存超过 1.7GB 的限制以及当事件循环经常卡住后你的 UX 是否仍正常运行?为了解决上面提到的这些问题, node-chaos (alpha) 提供了各种 Node 相关的噪声。

❌ 否则:墨菲定律一定会无情地砸中你的产品,跑不掉的。

👏 正例: Node-chaos 可以生成所有类型的 Node.js 问题,因此您可以测试您的应用程序对混乱的适应能力

⚪ 2.7 不要写全局的 fixtures 和 seeds,而是放在每个测试中

✅ 建议:参照黄金法则,每条测试需要在它自己的 DB 行中运行避免互相污染。现实中,这条规则经常被打破:为了性能提升而在执行测试前全局初始化数据库 (也被称为‘test fixture’)。尽管性能很重要,但是它可以通过后面讲的「分组件测试」缓和。为了减轻复杂度,我们可以在每个测试中只初始化自己需要的数据。除非性能问题真的非常显著,那么可以做一定的妥协 —— 仅在全局放不会改变的数据(比如 query)。

❌ 否则:一部分测试挂了,我们的团队花费大量宝贵时间后发现,是由于两个测试同时改变了同一个 seed 数据导致的。

👎 反例:用例之间不独立,而是依赖同一个全局钩子来生成全局 DB 数据

before(() => {
//adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework
await DB.AddSeedDataFromJson('seed.json');
});
it("When updating site name, get successful confirmation", async () => {
//I know that site name "portal" exists - I saw it in the seed files
const siteToUpdate = await SiteService.getSiteByName("Portal");
const updateNameResult = await SiteService.changeName(siteToUpdate, "newName");
expect(updateNameResult).to.be(true);
});
it("When querying by site name, get the right site", async () => {
//I know that site name "portal" exists - I saw it in the seed files
const siteToCheck = await SiteService.getSiteByName("Portal");
expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[
});

👏 正例:每个用例操作它自己的数据集

it("When updating site name, get successful confirmation", async () => {
//test is adding a fresh new records and acting on the records only
const siteUnderTest = await SiteService.addSite({
name: "siteForUpdateTest"
});

const updateNameResult = await SiteService.changeName(siteUnderTest, "newName");

expect(updateNameResult).to.be(true);
});

关于本文
原文:https://github.com/goldbergyoni/javascript-testing-best-practices/blob/master/readme-zh-CN.md

关于【实践】相关推荐,欢迎读者们自荐投稿,前端早读课等你来

【第2724期】前后端数据接口协作提效实践

【第2695期】基于微前端qiankun的多页签缓存方案实践

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存