likes
comments
collection
share

仁化大帝 puppeteer 和 妙化元君 jest

作者站长头像
站长
· 阅读数 58

这篇文章以零碎的知识点的方式列举了 puppeteer 结合 jest 测试的一些要点。这两个工具结合起来能够对前端项目中的功能有一个全面,准确,高效的测试覆盖。将这两个测试工具结合起来本身就是富有趣味的,不仅如此,它们也能切实为前端开发带来效率上的提升。测试同时也保证了项目功能的可扩展性,最大程度上减少了新增业务对原来功能的冲击。

从本文中你可以收获到:

  • 基本的测试流程和测试用例设计。
  • 如何将 jest 和 puppeteer 结合起来,以及为什么要结合起来。
  • 上述测试框架的搭建,测试环境的创建。
  • 工厂方法设计模式在测试中的使用及原因。
  • 如何 Mock 一个用户会话,setCookie 的使用。
  • 高级的对象 (Page) 扩展策略 -- 组合设计模式和代理设计模式。
  • Jest: describe 的嵌套、生命周期以及与测试用例树结构之间的映射关系。
  • puppeteer: 基本使用,常用方法以及测试 dom 或者 网络请求的方法。

1. unit 和 integration test 的不同:

仁化大帝 puppeteer 和 妙化元君 jest

这张图已经很清晰的写出了单元测试和集成测试的区别。单元测试是对独立的一小块进行测试的。而集成测试往往涉及多个这种小块联合起来实现的功能。

2. Testing flow

下面这张图展示了,在本文中将 jest 和 puppeteer 结合起来的基本过程,这是一个流程图,其中涉及到的重要概念有:Chromium headless assertion

仁化大帝 puppeteer 和 妙化元君 jest

这张图实际上展示的东西,很强。

3. 一些术语及解释

  • test suite 指的是一组用于测试软件或硬件的测试案例集合。
  • chromium 和 chrome 的发音,以及区别:Chromium 是一个开源的浏览器项目,由 Google 主导,它是 Google Chrome 浏览器的基础。Chromium 本身不包含 Google 的某些专有功能或插件。
  • high level overview 高级概述总体概览
  • blank slate 通常用来比喻一个新的开始,或者表示某物或某人在某方面是全新的,没有受到任何先前经验或信息的影响。就像一块干净的黑板,等待被书写新的内容。
  • in the fly 这个短语在非字面意义上有多重含义,但通常用来形容某事是在进行中或即时完成的。例如,在计算机领域中,“on the fly”可能指的是在数据被读取或处理的同时进行某种操作,而不是先存储数据然后再进行处理。此外,它也可以用来形容某人迅速地或即兴地做出决定或行动,而不是事先计划好的。
  • house a buntch of tests 中 house 的意思:"house" 意为容纳、包含或管理一系列测试。
  • gotcha 翻译:

4. 关于 headless

使用无头浏览器的主要目的就是为了 run faster, 这个 headless chromium 是 jest 的 test 发起的 puppeteer 的 launch 方法创建的浏览器实例。

5. 将 jest 和 puppeteer 结合起来进行测试的 3 个难点如下图所示:

仁化大帝 puppeteer 和 妙化元君 jest

即:
  • 如何从代码的角度和无头浏览器交互;
  • 如何在jest中发起断言,断言用户的交互;
  • 如何模仿用户的行为如登录。

6. 项目依赖

展示项目依赖,用来避免兼容性的问题。

仁化大帝 puppeteer 和 妙化元君 jest

7. puppeteer 工作的原理图示

仁化大帝 puppeteer 和 妙化元君 jest

8. 将 jest 和 puppeteer 结合起来的基本代码

仁化大帝 puppeteer 和 妙化元君 jest

也即是在一个 test 中使用 puppeteer 是精髓,但是需要注意的是,这样做的话一切都是异步的。

9. 通用测试流程

下图展示的实际上是一个通用型的测试用例的过程:

仁化大帝 puppeteer 和 妙化元君 jest

这个过程简单的叙述为:

  • 实例化浏览器-跳转至前端项目
  • 点击页面上面的元素(即于页面进行交互)
  • 找到响应元素并读取其内容
  • 使用断言判断内容是否符合预期。

用一段代码来表示上述的流程: 仁化大帝 puppeteer 和 妙化元君 jest

对于上图中的代码 await page.$eval('a.brand-logo', el => el.innerHTML); 你可能会感到疑问:为什么获取元素这么复杂?而下面几张图则非常深刻的解释了内在的原因:

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

总结一下就是:发起测试的 nodeJs 环境,和 puppeteer 发起的浏览器环境在代码上是隔离开来的;因此虽然它们使用的都是 js 作为开发语言。但是它们的函数和数据或者状态之间是无法相互调用的。就用上面的代码来说,el => el.innerHTML 这个在 nodeJS 环境中被定义的函数,被传给浏览器的时候是以 string 的方式。

理解为什么 puppeteer 的 API 给出的方法都是以上述代码的方式很重要!

10. 使用 jest 的钩子函数优化基础架构 - setup

如下图所示的代码,我想在一定程度上也说明了我们将 jest 和 puppeteer 结合起来的必要性。即:虽然 puppeteer 很强大,但是与 jest 结合之后,由于 jest 提供了一些辅助函数用于流程控制,能够让 puppeteer 发挥出更大的作用。

仁化大帝 puppeteer 和 妙化元君 jest

在上图的代码中,我们就使用了 jest 框架中的 describe 携带的生命周期函数 beforeEach 来为每一个 test 在正式运行之前提供 url 为 localhost:3000 的测试页面。

同样的,使用钩子函数 afterEach 可以在每一个测试完成之后关闭浏览器,而不是其他时机:

仁化大帝 puppeteer 和 妙化元君 jest

11. 检查当前 puppeteer 打开的页面是否为目标页面 -- 使用 toMatch 这个 matcher

除了提供 test 流程上面的控制,jest 强大的断言 API 也为 puppeteer 提供了足够的支持。我们可以使用名为 toMatch 的 matcher 来检查当前网页的地址是否为预期值。

下图是 .toMatch 方法的介绍:

仁化大帝 puppeteer 和 妙化元君 jest

12. 绕过谷歌登录图灵测试的几个方法

大多数网站在登陆的时候都会进行图灵校验来保证网页的安全性。在测试的时候,如果遇到了这类图灵测试,那该如何是好呢?以下图中列举了 3 种常见的解决方案。 仁化大帝 puppeteer 和 妙化元君 jest

  • 做一个隐藏的开发后门:缺点是不符合一个原则:最好不要仅仅为了测试而改动代码。
  • 测试的时候不要求鉴权:缺点是很难区分测试和生产环境,就算能也不好修改。
  • 做一个假的 session 冒充用户:这是普遍采用的解决方案。

13. Mock 一个用户会话

1. 用户会话的原理

下面这张时序图展示的就是用户会话的创建流程: 仁化大帝 puppeteer 和 妙化元君 jest 而我们能够 Mock 的部分其实就是从 Node 到 User 的最后一步。

仁化大帝 puppeteer 和 妙化元君 jest 也就是说如果我们能够模拟出表示用户身份的 cookie 就意味着我们可以跳过鉴权。下面的图反映的是 cookie 生成的原理: 仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

如果明白了上面图所示的签名过程,那么解决方案也就呼之欲出了:

仁化大帝 puppeteer 和 妙化元君 jest

使用keygrip和密钥对信息签名以及校验可以表示为:

仁化大帝 puppeteer 和 妙化元君 jest 仁化大帝 puppeteer 和 妙化元君 jest

生产环境下的密钥是通过全局变量设置的,我们只需读取密钥然后生成用户会话并签名即可:

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

2. 前端开发配置策略

Mock 用户会话的时候我们从本地读取了配置信息(其中包括了签名用的密钥)。现在我们实际上有三种环境:开发环境、测试环境、生产环境。每种环境需要的配置项是不一样的,下图就展示了如何在不同的环境下采用不同的配置项。

仁化大帝 puppeteer 和 妙化元君 jest

3. Mock 用户会话的全部代码

绕过谷歌检查的策略就是前端自己生成一个 session 然后使用 puppeteer 中的 setCookie 方法设置到浏览器中去。 仁化大帝 puppeteer 和 妙化元君 jest 仁化大帝 puppeteer 和 妙化元君 jest

4. 小节用户会话 Mock

前端能够自己制作一个假的用户会话有三个要点:

  • 需要知道会话签名的密钥,由于测试是在开发的时候进行的,开发者拿到密钥也算合理。
  • 需要知道数据库中的 user.id。
  • 使用 puppeteer 的 setCookie 方法将会话设置到浏览器中去。

14. puppeteer 提供的 waitFor

puppeteer 可以使用 waitFor 来等待某个元素的出现;然后才是尝试获取这个元素上面的一些信息。也就是先等待然后再获取。这很像 jest 种的 findByRole 方法的作用。不推荐自己写这种方法,自己写的没有官方的错误边界以及优化。

15. Test 种的 Factory 的思想

仁化大帝 puppeteer 和 妙化元君 jest

对于测试而言,很重要的一点在于独立性,也就是需要为每一次测试创建全新的用户对象和用户会话对象。这很重要。

因此,为了能够方便的产生这些对象,我们需要相应你的工厂方法,就如同上图所示的有 Session Factory 以及 User Factory 就是为了方便的产生我们之后测试中需要用到的响应对象的。

16. 插入一个使用 id 的时候的防错小技巧

id 是 string 类型的!并且一般情况下,环境也能够提供字符串供我们使用。但是!为了防止不必要的问题,我们可以使用下图所示的小技巧:

仁化大帝 puppeteer 和 妙化元君 jest

17. mongoose Schema 以及创建 User 工厂

上面提到的测试中使用的工厂是通过数据库如 mongoose 等,的 Schema 提供的。

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

引入 Factory 之后的测试代码如下所示:

仁化大帝 puppeteer 和 妙化元君 jest

上述代码中的知识点总结如下:

  • Factory 的使用,可以用来简化代码。
  • 使用 setCookie 向浏览器实例设置 cookie 完成用户会话的 Mock。
  • 使用 goto 方法可以完成页面的跳转。
  • 使用 waitFor 方法等待某个元素的出现,类似于 jest 中的 findBy 或者 findAllBy。
  • 使用 page.$eval 将函数以字符串的形式从一个环境传递到另一个环境中。

18. jest 测试启动脚本

在所有的 jest 测试运行之前,有的时候需要做一些基础设施,比如:链接数据库或者初始化 mgdb 的 schema 等,这个时候我们可以通过一个 js 文件引入环境变量或者链接到数据库,只需要在 package.json 中新增如下对 jest 的配置即可!

仁化大帝 puppeteer 和 妙化元君 jest

而初始化文件的内容为:

仁化大帝 puppeteer 和 妙化元君 jest

19. 增强 Page 类

不难看出, Page 对象是我们用于和页面进行交互的媒介。虽然 Page 实例提供了大量的内置方法,但是这些方法颗粒度太细了,用起来不是很方便。这基于一个现象,那就是针对同一个前端项目,其测试代码的很多部分都是可以复用的。

1. 在原型上设置自定义方法

因此有必要在 Page 类上添加我们自己的方法。而首当其冲的方法就是在原型上设置方法,例如:

仁化大帝 puppeteer 和 妙化元君 jest

这曾经是业内常见的做法,但现在被弃用了。

2. 自定义类继承原来的 Page 类

为什么直接修改原型的方式是不好的呢?一个明显的缺陷就是这种方式的侵入性的,容易带来额外的“惊喜”。在 ES6+ 中,我们可以方便的使用类继承的方式自己制作一个类,并在子类上实现我们的自定义方法。 仁化大帝 puppeteer 和 妙化元君 jest

3. 组合设计模式

上面介绍的继承的方法是完全 ok 的。但是有一个问题,那就是代码中的 Page 实例是通过 broswer.newPage 得到的。也就是说 CustomPage 很好,但是不会被自动创建出来,因此只通过继承的方式显然是不够的,我们需要更加高级的方法。

下面的方法采用了组合的设计模式,将原生的 Page 实例包装了起来,然后在此基础之上做了扩充。

class Page {  
  goto(url) {  
    console.log(`I'm going to ${url}`);  
  }  
    
  setCookie() {  
    console.log('I\'m setting a cookie');  
  }  
}  
  
class CustomPage {  
  constructor(page) {  
    this.page = page;  
  }  
    
  login() {  
    this.page.goto('localhost:3000');  
    this.page.setCookie();  
  }  
}

4. 王炸:组合设计模式和代理设计模式

其实上面写的组合设计模式已经很好了,在 CustomPage 的实例中,可以轻松的通过 .page.go 来执行原生 Page 实例上面的方法。但如果我们在技术上仍有更高的追求的话,可以尝试在此基础之上使用代理设计模式,达到更高层次的封装。

下面几张图旨在说明代理设计模式以及在 js 中使用代理设计模式的流程。 仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

如果你对代理设计比较熟悉的话,那么就知道代理模式实际上提供了我们操作一个 Js 对象的更高权限。通过代理模式我们可以直接入侵到一个对象的方法中去自定义一些我们定制的逻辑。更为重要的是:这种对于对象属性的侵入操作可以是批量操作的!

结合了组合设计模式和代理设计模式的结果如下所示:

const puppeteer = require('puppeteer');  
  
class CustomPage {  
    constructor(page) {  
        this.page = page;  
    }  
  
    // 添加你需要的方法或属性  
  
    // 示例方法,用于访问页面并返回页面标题  
    async getTitle() {  
        return await this.page.title();  
    }  
}  
  
static async build() {  
    const browser = await puppeteer.launch({  
        headless: false  
    });  
    const page = await browser.newPage();  
    const customPage = new CustomPage(page);  
  
    return new Proxy(customPage, {  
        get: function(target, property) {  
            // 首先尝试从CustomPage实例中获取属性  
            if (property in target) {  
                return target[property];  
            }  
            // 如果CustomPage实例中没有该属性,尝试从page对象中获取  
            if (property in target.page) {  
                return target.page[property];  
            }  
            // 如果page对象中也没有,最后尝试从browser对象中获取  
            if (property in browser) {  
                return browser[property];  
            }  
            return undefined;  
        }  
    });  
}  
  
// 使用示例  
(async () => {  
    const myPage = await CustomPage.build();  
    console.log(await myPage.getTitle()); // 访问CustomPage的getTitle方法  
    // 你可以通过代理访问page或browser的其他属性和方法  
})();

可以肯定的是,上面的代码是通往高级前端工程师路上不能回避的高级技巧。

这样一来,我们就可以使用 .goTo 或者 .setCookie 来直接调用原来必须通过 .page.goTo 或 .page.setCookie 才能使用的方法了。

下面是一些优化之后的代码案例,注意 Page 不再是 puppeteer 中的类,而是来自本地文件 ./helpers/page 中。Page 类提供了一个静态方法 build 用来产生 Page 实例。而与测试无关的 broswer 等无关项则是被隐藏了起来。

  • 结合 jest 生命周期使用 仁化大帝 puppeteer 和 妙化元君 jest

  • 可以添加任意多的方法探索对象,在这里对具体的某个方法,先在本实例上寻找,然后是原生 Page 实例,最后才是 Browser 实例(这简直就是我们为每一个属性自定义了“原型链”) 仁化大帝 puppeteer 和 妙化元君 jest

  • 方便的添加自定义方法 仁化大帝 puppeteer 和 妙化元君 jest

仁化大帝 puppeteer 和 妙化元君 jest

20.为什么 jest 需要和 puppeteer 结合起来使用?

  • jest 需要 puppeteer 因为 puppeteer 可以提供了操控浏览器的权限,例如上面的设置 cookie , 这个在 jest 中是万万做不到的。因此 puppeteer 适合做页面测试,比 jest 高一个维度,测试的范围更广。
  • 而 puppeteer 需要 jest 的原因也很简单。 jest 是一个测试框架,提供了诸如测试管理、测试并发、断言等众多 API 方便测试过程能够有条不紊的进行下去。能够极大的简化 puppeteer 的测试流程。
  • 因此将 jest 和 puppeteer 结合起来属于强强联合。在单元测试的领域 jest 独领风骚,而在 puppeteer 的领域,也需要 jest 的协助,因此虽然 puppeteer 很强,但是结合 jest 使用则会更强。

21. jest 超时时间设置

默认情况下,在 jest 中,异步函数超过 5 秒就会被认定失败了

仁化大帝 puppeteer 和 妙化元君 jest

解决方案其实也很简单,就是在 jest 的启动文件 setup.js 中设置超时时间为 30s

仁化大帝 puppeteer 和 妙化元君 jest

上面设置了超时时间为 30s. 注意这个时间指的是每一个 test 的超时时间,而不是总共的超时时间。

21. 测试用例设计

下图是用户登录然后发送博客这个过程的测试用例设计。其中蓝色背景表示一个 wrapper 对应的是 jest 中的 describe, 而绿色背景则是每一个被 describe 包裹的 test.

仁化大帝 puppeteer 和 妙化元君 jest

describe 和 test 的区别

这里主要在说我们应该将拥有相同 setup 的 tests 归结成为一个相同的 describe 中;此外还有 describe statement 和 test statement 的写法上的区别:

仁化大帝 puppeteer 和 妙化元君 jest

使用了 describe 成组之后的测试的写法:

仁化大帝 puppeteer 和 妙化元君 jest

然后再看下面这幅图,从图上可以清楚的看到,我们在一个 describe 中嵌入另外一个 describe ,这是完全合法的。而且只要你明白,嵌套代表着 树状结构,或者表示的是业务 流程图 的概念,那么 describe 之间的嵌套就是有意义的。

仁化大帝 puppeteer 和 妙化元君 jest

describe 的意义

在 Jest 测试框架中,describe 函数用于将测试用例分组,以便更好地组织和管理测试用例。每个 describe 块可以包含多个测试用例(testit 函数),也可以嵌套其他的 describe 块,形成一个层次化的测试结构。

嵌套的 describe 块在测试中起到的作用主要有以下几点:

  1. 更好的组织:通过嵌套 describe 块,你可以根据测试的不同方面或模块来组织测试用例。例如,你可以按照功能、组件或业务逻辑来分组测试,使得测试代码更加清晰和易于理解。

  2. 上下文设置:嵌套的 describe 块可以用于设置不同层次的测试上下文。在外层的 describe 块中,你可以设置一些共享的测试中境或数据,而在内层的 describe 块或测试用例中,你可以基于这些共享环境或数据进行更具体的测试。

  3. 描述性更强:通过嵌套的 describe 块,你可以为每个测试组提供更详细的描述,这有助于其他人理解测试的目的和范围。特别是当测试代码库变得庞大时,这种层次化的描述方式非常有助于定位和理解特定的测试组。

  4. 共享设置和清理:在外层 describe 块中,你可以使用 beforeAllbeforeEachafterAllafterEach 钩子函数来设置和清理测试环境。这些钩子函数将在每个 describe 块的所有测试用例之前或之后执行,从而避免了在每个单独的测试用例中重复设置和清理代码的冗余。

  5. 更好的测试报告:当使用 Jest 生成测试报告时,嵌套的 describe 块可以帮助你更清晰地看到哪些测试用例通过了,哪些没有通过。这种层次化的报告结构使得定位和修复问题变得更加容易。

总的来说,嵌套的 describe 块在 Jest 测试中起到了组织测试用例、设置测试上下文、增强测试描述性、共享设置和清理以及生成更好的测试报告等作用。

22. describe 的生命周期

describe 不仅可以嵌套,并且这些嵌套着的 describe 每一个都可以拥有自己的 生命周期函数,这是相当方便的:

仁化大帝 puppeteer 和 妙化元君 jest

23. 树状图对应的测试代码

仁化大帝 puppeteer 和 妙化元君 jest 或者

仁化大帝 puppeteer 和 妙化元君 jest 仁化大帝 puppeteer 和 妙化元君 jest

上面的代码用到了:

  • .type 可以给表单组件中输入字符。
  • .click 可以用来聚焦 dom 元素或者触发按钮。
  • .getContentsOf 是我们自行封装的用来获取元素内容的自定义方法。

24. 测试 API

使用 puppeteer 甚至可以让浏览器去发出一个 post 请求。当然调用 API 本身就是测试的内容。但是这里提供一个具象的理由将这种行为解释一下:上面代码中使用 waitFor 这种内置方法测试元素的存在性或者内容比起真正的调用 API 获取渲染数据来说只能称之为间接方法,而不是能够进行验证的直接方法,所以我们确实有时候需要从源头解决问题。

仁化大帝 puppeteer 和 妙化元君 jest

而我们用来测试 API 的流程大概可以表示为:

仁化大帝 puppeteer 和 妙化元君 jest

这里需要注意一下,post 请求是通过浏览器对象发送出去的,而不是 jest

25. 如何发起请求

axios 虽然很好用,但是并不是被浏览器天然支持的。特别是在测试的时候使用的是开源的 chromium , 其中就更不可能自带 axios 这种第三方库了。因此,在测试环境中最好还是直接使用内置的 fetch 更为妥当。这也可以看成是 fetch 存在的一个用法。

下面是使用 fetch 请求数据的一个截图:

仁化大帝 puppeteer 和 妙化元君 jest

如果想要在 puppeteer 的测试环境下面使用 fetch 来请求数据,那么需要将 fetch 的执行包裹在 page.evaluate 这个函数中,此函数的官方介绍如下所示:

仁化大帝 puppeteer 和 妙化元君 jest

下面是 jest 结合 puppeteer 进行的 API 测试,不难看出来和纯单元测试不同,puppeteer 提供了更加真实的测试环境:

describe('User is not logged in', () => {  
  test('User cannot create blog posts', async () => {  
    const result = await page.evaluate(async () => {  
      return fetch('/api/blogs', {  
        method: "POST", // 修正了引号和逗号  
        credentials: 'same-origin',  
        headers: {  
          'Content-Type': 'application/json' // 修正了引号和逗号  
        },  
        body: JSON.stringify({ title: 'My Title', content: 'My Content' }) // 修正了字符串内容和引号  
      }).then(res => res.json());  
    });  
    console.log(result);  
    expect(result).toEqual({ error: 'You must log in to perform this action' });
  });  
});

注意各个函数的返回值在各个环境之间的传递路线和策略


再次强调一下,使用 jest 的根本原因是它可以并行测试 puppeteer 的用例。

上面的示例用了 post 请求作为示例,对于 get 请求,当然也是可以的!

test('User cannot get a list of posts', async () => {  
  const result = await page.evaluate(async () => {  
    return fetch('/api/blogs', {  
      method: 'GET',  
      credentials: 'same-origin',  
      headers: {  
        'Content-Type': 'application/json'  
      }  
    }).then(res => res.json());  
  });  
  
  expect(result).toEqual({ error: 'You must log in!' });  
});

credentials: 'same-origin' 的作用

credentials: 'same-origin'fetch API 中的一个选项,用于控制在跨域请求中是否发送 cookies、HTTP authentication 或 client side SSL certificates。这个选项影响着请求中的凭据信息如何被处理。

credentials 选项可以有三个可能的值:

  1. 'same-origin':这是默认值。当设置为 'same-origin' 时,如果请求是同源的(即请求的 URL 与页面的源相同),则会发送凭据(如 cookies、HTTP authentication 等)。如果请求是跨域的,那么不会发送凭据。这有助于保护用户的隐私和安全,防止跨域请求滥用用户的凭据信息。

  2. 'include':当设置为 'include' 时,无论是同源请求还是跨域请求,都会发送凭据。这通常用于那些需要用户身份验证的跨域 API 请求。

  3. 'same-site'(已弃用):这个值的行为与 'same-origin' 类似,但它还允许在相同站点的情况下发送凭据,即使是通过顶级导航发起的跨域请求。然而,由于这个值在实际应用中引起了混淆,并且与新的 Cookie SameSite 属性有潜在的冲突,因此已被 Web 标准弃用。

在安全性敏感的上下文中,正确地设置 credentials 选项非常重要。例如,如果你的 Web 应用需要用户登录,并且你正在使用 cookies 来存储会话信息,那么你可能希望在跨域请求中包含这些凭据。但是,你也需要确保目标服务器正确地处理这些凭据,并且只接受来自可信源的请求,以防止跨站请求伪造(CSRF)等安全漏洞。

总的来说,credentials: 'same-origin' 是一种保守而安全的选择,它确保了只有在请求与页面同源时才会发送凭据信息。

26. 将 API 测试的 get 或者 post 请求封装成为 customPage 类上的方法:

get(path) {  
  return this.page.evaluate((_path) => {  
    return fetch(_path, {  
      method: 'GET',  
      credentials: 'same-origin',  
      headers: {  
        'Content-Type': 'application/json'  
      }  
    }).then(res => res.json());  
  }, path); // 注意这里的逗号,它将 evaluate 的函数参数和 path 参数分隔开  
}

这样只需要使用 page.get() 就可以进行 API 返回值的获取了。

需要注意的事情是,由于 evaluate 的第一个参数在 nodejs 环境下为函数,并且需要变成字符串之后才能传递给浏览器环境。因此,此函数使用的参数需要通过其它途径再次传递。因此 evaluate 的第二个参数就是用来传递 url 的,你可以认为是做了某种等价替换(字符串层面上的)

当然也可以挂载一个 post 请求,其代码如下,注意这里应该是 this.page.evaluate 而不是 page.evaluate

post(path, data) {  
  return this.page.evaluate(  
    (_path, _data) => {  
      return fetch(_path, {  
        method: 'POST',  
        credentials: 'same-origin',  
        headers: {  
          'Content-Type': 'application/json'  
        },  
        body: JSON.stringify(_data)  
      }).then(res => res.json());  
    },  
    path, // 传递给evaluate的第一个参数  
    data  // 传递给evaluate的第二个参数  
  );  
}

注意这里传递的第二个变量 data 在传递之前无需序列化。所以可能不是简单的字符串替换,有更加复杂的机制。

27. 使用遍历的方式测试 API 集

一旦我们有了类似于 get 或者 post 的辅助方法,我们就可以用类似于配置的方法对所有 scope 中的接口进行测试了。下面的例子中,我们做了一个辅助方法,结合 iteratormapPromise.all 对可并行(指的是没有先后的依赖关系)的接口进行了测试。

具体代码实现如下:

const actions = [  
  { method: 'get', path: '/api/blogs' },  
  { method: 'post', path: '/api/blogs', data: { title: 'T', content: 'C' } }  
];  
  
// 测试函数,这里假设使用了某种测试框架(如Jest)  
test('Blog related actions are prohibited', async () => {  
  const page = {  
    // 假设page对象提供了execRequests方法和其他HTTP方法  
    execRequests: execRequests,  
    get: jest.fn(() => Promise.reject({ error: 'You must log in!' })),  
    post: jest.fn(() => Promise.reject({ error: 'You must log in!' }))  
    // 其他HTTP方法也可以在这里模拟,如put, delete等  
  };  
  
  const results = await page.execRequests(actions);  
  for (let result of results) {  
    expect(result).toEqual({ error: 'You must log in!' });  
  }  
});  
  
// execRequests函数的实现  
async function execRequests(actions) {  
  const results = [];  
  for (const action of actions) {  
    try {  
      const { method, path, data } = action;  
      const response = await this[method](path, data);  
      results.push(response);  
    } catch (error) {  
      results.push(error);  
    }  
  }  
  return results;  
}

喜欢的同学收藏起来吧!说不定哪天裁员了我们还可以去干测试,cool!

转载自:https://juejin.cn/post/7371384453359534095
评论
请登录