Mock 网页地址

上一章说到可以配置 testEnvironment: 'jsdom' 来创造一个 Node.js 的浏览器环境,然后在这个环境下跑测试。但,真的只配一个 jsdom 就解决所有问题了么?

当然不是!其中最难搞的场景就是修改网页路径。

例子

我们这里依然用一个例子来说明,添加 src/utils/getSearchObj.ts

  1. // src/utils/getSearchObj.ts
  2. const getSearchObj = () => {
  3. // ?a=1&b=2
  4. const { search } = window.location;
  5. // a=1&b=2
  6. const searchStr = search.slice(1);
  7. // ['a=1', 'b=2']
  8. const pairs = searchStr.split("&");
  9. // { 'a': '1' }
  10. const searchObj: Record<string, string> = {};
  11. pairs.forEach((pair) => {
  12. // [a, 1]
  13. const [key, value] = pair.split("=");
  14. searchObj[key] = value;
  15. });
  16. return searchObj;
  17. };
  18. export default getSearchObj;

这个函数的作用是把网页地址中的 查询参数字符串 转换为 对象,比如:

  1. window.location.href = 'https://www.baidu.com?a=1&b=2'
  2. const result = getSearchObj()
  3. // result = {
  4. // a: '1',
  5. // b: '2',
  6. // }

现在我们就按这个例子来给 getSearchObj 写测试,添加 tests/utils/getSearchObj.test.ts

  1. // tests/utils/getSearchObj.test.ts
  2. import getSearchObj from "utils/getSearchObj";
  3. describe("getSearchObj", () => {
  4. it("可以获取当前网址的查询参数对象", () => {
  5. window.location.href = "https://www.baidu.com?a=1&b=2";
  6. expect(window.location.search).toEqual("?a=1&b=2");
  7. expect(getSearchObj()).toEqual({
  8. a: "1",
  9. b: "2",
  10. });
  11. });
  12. it("空参数返回空", () => {
  13. window.location.href = "https://www.baidu.com";
  14. expect(window.location.search).toEqual("");
  15. expect(getSearchObj()).toEqual({});
  16. });
  17. });

然后你会得到这个报错:

Mock 网页地址 - 图1

扩展测试环境

为什么明明设置了新的 window.location.href,还是为空的呢?根据 这个 StackOverflow 贴子 , 你必须要用下面代码才能修改当前的(假的)网页地址:

  1. import { JSDOM } from 'jsdom';
  2. const jsdom = new JSDOM();
  3. // ...
  4. jsdom.reconfigure({
  5. url: 'https://www.baidu.com?a=1&b=2',
  6. });
  7. // ...

那么问题来了:这里的 jsdom 是从哪里来的呢? 如果你尝试用 global.jsdom 或者 global.JSDOM 来生成 jsdom,然后调用 jsdom.reconfigure,你会得到在一个大大的 undefined。 因为 Jest 并没有把 jsdom NPM 的内容暴露出来,导致你无法使用 jsdom.reconfigure。详见 这个 Github Issue

有的同学会留意到:刚刚在 Mock localStorage 的时候,我们用到了 Object.defineProperty,那我们能否用下面的方法来试图 Hack 掉网页地址呢?

  1. Object.defineProperty(window.location, 'href', {
  2. writable: true,
  3. value: 'https://www.baidu.com?a=1&b=2'
  4. });

答案是:不行! 你会得到这样的报错:Error: Not implemented: navigation (except hash changes),毕竟是 Hack 手法,并不推荐,详见这个 Issue

::: tip 经 Issue 区提醒,也可以尝试以下方法:

  1. describe('getSearchObj', () => {
  2. it('可以获取当前网址的查询参数对象', () => {
  3. Object.defineProperty(window, 'location', {
  4. writable: true,
  5. value: { href: 'https://google.com?a=1&b=2', search: '?a=1&b=2' },
  6. });
  7. expect(window.location.search).toEqual('?a=1&b=2');
  8. expect(getSearchObj()).toEqual({
  9. a: '1',
  10. b: '2',
  11. });
  12. });
  13. it('空参数返回空', () => {
  14. Object.defineProperty(window, 'location', {
  15. writable: true,
  16. value: { href: 'https://google.com', search: '' },
  17. });
  18. expect(window.location.search).toEqual('');
  19. expect(getSearchObj()).toEqual({});
  20. });
  21. });

这个方法与上面不同点在于:Mock window.location 对象,而不是 window.location.href 属性。但缺点是不仅要在 href 写查询参数,还要在 search 再写一遍查询参数。 :::

终于,有人受不了,不就 jest 没有把 jsdom 对象丢到全局么?把 jsdom 测试环境做个扩展不就好了:

  1. const JSDOMEnvironment = require("jest-environment-jsdom");
  2. module.exports = class JSDOMEnvironmentGlobal extends JSDOMEnvironment {
  3. constructor(config, options) {
  4. super(config, options);
  5. // 放到全局
  6. this.global.jsdom = this.dom;
  7. }
  8. teardown() {
  9. this.global.jsdom = null;
  10. return super.teardown();
  11. }
  12. };

上面这段代码继承了原来的 JSDOMEnvironment 的测试环境,在构造器里把 jsdom 绑定到了全局对象上。

当然,我们不用自己写这段代码,有人已经把它变成了一个 NPM 包了:jest-environment-jsdom-global 。 我们来安装一下:

  1. npm i -D jest-environment-jsdom-global@3.0.0

然后在 jest.config.js 里使用这个魔改后的测试环境:

  1. // jest.config.js
  2. module.exports = {
  3. testEnvironment: 'jest-environment-jsdom-global'
  4. };

再把测试改成如下:

  1. // src/utils/getSearchObj.ts
  2. import getSearchObj from "utils/getSearchObj";
  3. describe("getSearchObj", () => {
  4. it("可以获取当前网址的查询参数对象", () => {
  5. // 使用全局暴露出来的 jsdom
  6. global.jsdom.reconfigure({
  7. url: "https://www.baidu.com?a=1&b=2",
  8. });
  9. expect(window.location.search).toEqual("?a=1&b=2");
  10. expect(getSearchObj()).toEqual({
  11. a: "1",
  12. b: "2",
  13. });
  14. });
  15. it("空参数返回空", () => {
  16. // 使用全局暴露出来的 jsdom
  17. global.jsdom.reconfigure({
  18. url: "https://www.baidu.com",
  19. });
  20. expect(window.location.search).toEqual("");
  21. expect(getSearchObj()).toEqual({});
  22. });
  23. });

由于 global 类型声明中没有声明 jsdom 属性,导致下面的报错:

Mock 网页地址 - 图2

所以,我们还要添加一个全局声明文件 src/types/global.d.ts

  1. // src/types/global.d.ts
  2. declare namespace globalThis {
  3. var jsdom: any;
  4. }

::: danger 上面属性声明一定要用 var!否则不生效! :::

配置好后,测试文件就不会报错了。

Mock Location

上面的做法不是很优雅:我们只是想改个地址而已,又要改环境,又要写全局类型定义,而且还是个 any 类型,动静有点大。 有没有动静小一点的方法呢? 有,我们可以用 jest-location-mock

::: tip 要实现这样的小功能,你永远可以相信 NPM 😘。 :::

这个包就是专门用于修改网页地址的。缺点是我们只能用它 Mock 的 3 个 API:

  • window.location.assign
  • reload
  • replace

不过,我们这个场景是完全够用的。先来安装一波:

  1. npm i -D jest-location-mock@1.0.9

然后在 setup 文件 tests/jest-setup.ts 里全局引入一下:

  1. // jest-setup.ts
  2. // 使用 Jest 的 Spy 和扩展 expect 来 Mock `window.location`
  3. import "jest-location-mock";

再把 jest.config.js 里的 testEnvironment 改回来:

  1. // jest.config.js
  2. module.exports = {
  3. testEnvironment: 'jsdom' // 改回来
  4. };

最后,把测试代码改为:

  1. // src/utils/getSearchObj.ts
  2. import getSearchObj from "utils/getSearchObj";
  3. describe("getSearchObj", () => {
  4. it("可以获取当前网址的查询参数对象", () => {
  5. window.location.assign('https://www.baidu.com?a=1&b=2');
  6. expect(window.location.search).toEqual("?a=1&b=2");
  7. expect(getSearchObj()).toEqual({
  8. a: "1",
  9. b: "2",
  10. });
  11. });
  12. it("空参数返回空", () => {
  13. window.location.assign('https://www.baidu.com');
  14. expect(window.location.search).toEqual("");
  15. expect(getSearchObj()).toEqual({});
  16. });
  17. });

再执行一下测试用例,会发现测试通过。

总结

在这一章里,我们学到了两种修改测试中网页地址的方法:

第一种方法使用 jest-environment-jsdom-global 这种方法会在 global 全局对象挂一个 jsdom, 通过 jsdomreconfigure API 来修改 JSDOM 环境的 url

第二种方法使用 jest-location-mock 这种方法会监听 window.location.assign,通过它来改变网页地址。

两种方法任君选择,我会比较推荐使用 jest-locatin-mock,简单一点。

“如何在测试中修改网页地址” 是前端测试中非常常见的一个问题,在中文社区里几乎好的回答,很多还停留在 Object.defineProperty。然而,这个方法早在 jest@21.x 的版本就用不了了。

所以,我觉得有必要开一章告诉大家正确修改 window.location.href 的方法。同时在这一章里,也给大家展示了如何通过继承 jsdom 环境来扩展成自己想要的测试环境。