6.5 处理 WebSockets 和 HTTP 请求

在本章的前几节中,您已经构建了一个在本地存储数据的前端应用程序。由于您的客户不共享后端,当多个用户更新库存时,每个人都会看到不同的项目列表。

在本节中,要在客户端之间同步项目,您将在第 4 章中将前端应用程序与后端集成,并学习如何测试该集成。在本节结束时,您将拥有一个可以读取、插入和更新数据库项目的应用程序。为了避免用户必须刷新页面才能看到其他人所做的更改,您还将实现实时更新,这将通过 WebSockets 发生。

注意您可以在 https://github.com/lucasfcosta/testing-javascript-applications 找到上一章后端的完整代码。

该后端将处理来自 Web 客户端的请求,为其提供数据并更新数据库条目。

为了让本章专注于测试并确保服务器支持我们正在构建的客户端,我强烈建议您使用我推送到 GitHub 的后端应用程序。它已经包含一些更新以更好地支持以下示例,因此您不必自己更改后端。

要运行它,请导航到chapter6/5_web_sockets_and_http_requests 中名为server 的文件夹,使用npm install 安装其依赖项,运行npm run migrate:dev 以确保您的数据库具有最新架构,并使用npm start 启动它。

如果您想自己更新后端,在服务器文件夹中有一个 README.md 文件,其中详细说明了我必须对我们在第 4 章中构建的应用程序所做的所有更改。

6.5.1 涉及 HTTP 请求的测试
通过将用户添加到库存中的项目保存到数据库来开始您的后端集成。为了实现这个功能,每当用户添加一个项目时,向我从第 4 章添加到服务器的新 POST /inventory/:itemName 路由发送一个请求。这个请求的正文应该包含添加的数量。

更新 addItem 函数,以便在用户添加项目时向后端发送请求,如下所示。

const data = { inventory: {} };
 
const API_ADDR = "http://localhost:3000";
 
const addItem = (itemName, quantity) => {
  const currentQuantity = data.inventory[itemName] || 0;
  data.inventory[itemName] = currentQuantity + quantity;
 
  fetch(`${API_ADDR}/inventory/${itemName}`, {               ❶
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ quantity })
  });
 
  return data.inventory;
};
 
module.exports = { API_ADDR, data, addItem };

❶ 添加商品时向库存发送 POST 请求

在您编写从库存中检索项目的请求之前,让我们讨论一下测试您刚刚实现的功能的最佳方法是什么。您将如何测试 addItem 函数是否与您的后端正确连接?

测试这种集成的一种次优方法是启动您的服务器并允许请求到达它。乍一看,这似乎是最直接的选择,但实际上,它需要更多的工作并产生更少的收益。

必须运行后端才能让客户的测试通过会增加测试过程的复杂性,因为它涉及太多步骤并为人为错误创造了太多空间。开发人员很容易忘记他们必须运行服务器,甚至更容易忘记服务器应该侦听哪个端口或数据库应该处于哪种状态。

尽管您可以自动执行这些步骤,但最好避免它们。最好将这种集成留给端到端 UI 测试,您将在第 10 章中了解。通过避免必须使用后端来运行客户端的测试,您还可以更轻松地进行设置将在远程环境中执行测试的持续集成服务,我将在第 12 章中介绍。

考虑到您不想让后端参与这些测试,您只有一种选择:使用测试替身来控制要获取的响应。你可以通过两种方式做到这一点:你可以存根 fetch 自身,编写断言以检查它是否被充分使用,并指定一个硬编码的响应。或者您可以使用 nock 来代替服务器的必要性。使用 nock,您可以确定要匹配哪些路由以及要提供哪些响应,从而使您的测试与实现细节更加分离,例如您传递给 fetch 的参数,甚至您使用哪些库来执行请求。由于我之前在第 4 章中提到的这些优点,我建议您采用第二种选择。

因为 nock 取决于到达拦截器的请求,首先,请确保您的测试可以在节点内运行并且它们可以分派请求。为此,请运行您的测试,看看会发生什么。运行它们时,您会注意到所有调用 handleAddItem 的测试都将失败,因为“未定义 fetch”。

尽管 fetch 在浏览器上是全局可用的,但它尚未通过 JSDOM 可用,因此,您需要找到一种方法来将其替换为等效的实现。要覆盖它,您可以使用一个设置文件,该文件将 isomorphic-fetch(一种可以在 Node.js 中运行的 fetch 实现)附加到全局命名空间。

使用 npm install --save-dev isomorphic-fetch 将 isomorphic-fetch 作为开发依赖项安装,并创建一个 setupGlobalFetch.js 文件,该文件会将其附加到全局命名空间。

const fetch = require("isomorphic-fetch");
 
global.window.fetch = fetch;           ❶

创建此文件后,将其添加到 jest.config.js 的 setupFilesAfterEnv 属性中的脚本列表中,如下面的代码所示,以便 Jest 可以在您的测试之前运行它,使 fetch 对它们可用。

module.exports = {
  setupFilesAfterEnv: [
    "/setupGlobalFetch.js",
    "/setupJestDom.js"
  ]
};

在这些更改之后,如果您没有可用的服务器,您的测试应该会失败,因为 fetch 发出的请求无法得到响应。

最后,是时候使用 nock 来拦截对这些请求的响应了。

将 nock 安装为开发依赖项(npm install --save-dev nock),并更新您的测试,以便它们具有 /inventory 路由的拦截器。

const nock = require("nock");
const { API_ADDR, addItem, data } = require("./inventoryController");
 
describe("addItem", () => {
  test("adding new items to the inventory", () => {
    nock(API_ADDR)                                      ❶
      .post(/inventory\/.*$/)
      .reply(200);
 
    addItem("cheesecake", 5);
    expect(data.inventory.cheesecake).toBe(5);
  });
});

❶ 响应所有对 POST /inventory/:itemName 的 post 请求

尝试仅为该文件运行测试。 为此,请将其名称作为第一个参数传递给 Jest。 你会看到测试通过了。

现在,添加一个测试以确保已到达 POST /inventory/:itemName 的拦截器。

// ...
 
afterEach(() => {
  if (!nock.isDone()) {                     ❶
    nock.cleanAll();
    throw new Error("Not all mocked endpoints received requests.");
  }
});
 
describe("addItem", () => {
  // ...
 
  test("sending requests when adding new items", () => {
    nock(API_ADDR)
      .post("/inventory/cheesecake", JSON.stringify({ quantity: 5 }))
      .reply(200);
 
    addItem("cheesecake", 5);
  });
});

❶ 如果在测试之后,并非所有拦截器都已到达,则清除它们并抛出错误

作为练习,继续使用 nock 拦截所有其他到达此路由的测试中对 POST /inventory/:itemName 的请求。如果您需要帮助,请查看本书的 GitHub 存储库,网址为 https://github.com/lucasfcosta/testing-javascript-applications。

在更新其他测试时,不要忘记在多个集成级别检查特定操作是否调用此路由。例如,我建议向 main.test.js 添加一个测试,以确保在通过 UI 添加项目时到达正确的路由。

提示拦截器一旦到达就会被移除。为了避免测试因为 fetch 无法得到响应而失败,你必须在每次测试之前创建一个新的拦截器,或者使用 nock 的 persist 方法,正如我们在第 4 章中看到的。

要完成此功能,您的前端必须在加载时向服务器询问库存项目。更改后,只有在无法到达服务器时才应将数据加载到 localStorage 中。

// ...
const { API_ADDR, data } = require("./inventoryController");
 
// ...
 
const loadInitialData = async () => {
  try {
    const inventoryResponse = await fetch(`${API_ADDR}/inventory`);
    if (inventoryResponse.status === 500) throw new Error();
 
    data.inventory = await inventoryResponse.json();
    return updateItemList(data.inventory);                        ❶
  } catch (e) {
    const storedInventory = JSON.parse(                           ❷
      localStorage.getItem("inventory")
    );
 
    if (storedInventory) {
      data.inventory = storedInventory;
      updateItemList(data.inventory);
    }
  }
};
 
module.exports = loadInitialData();

❶ 如果请求成功,则使用服务器的响应更新项目列表

❷ 如果请求失败,则从 localStorage 恢复库存

即使您的应用程序正在运行,main.test.js 中检查项目在会话之间是否持续的测试也应该失败。 它失败是因为在尝试从 localStorage 加载数据之前,它需要对 /inventory 的 GET 请求失败。

要使该测试通过,您需要进行两项更改:您必须使用 nock 使 GET /inventory 响应错误,并且您必须等到初始数据加载完毕。

// ...
 
afterEach(nock.cleanAll);
 
test("persists items between sessions", async () => {
  nock(API_ADDR)                                             ❶
    .post(/inventory\/.*$/)
    .reply(200);
 
  nock(API_ADDR)                                             ❷
    .get("/inventory")
    .twice()
    .replyWithError({ code: 500 });
 
  // ...
 
  document.body.innerHTML = initialHtml;                     ❸
  jest.resetModules();
 
  await require("./main");                                   ❹
 
  // Assertions...
});
 
// ...

❶ 成功响应 POST /inventory/:itemName 请求

❷ 两次回复错误请求到 GET /inventory

❸ 这相当于重新加载页面。

❹ 等待初始数据加载

不要忘记这些测试包含一个 beforeEach 钩子,因此,在其中,您还必须等待 loadInitialData 完成。

// ...
 
beforeEach(async () => {
  document.body.innerHTML = initialHtml;
 
  jest.resetModules();
 
  nock(API_ADDR)                               ❶
    .get("/inventory")
    .replyWithError({ code: 500 });
  await require("./main");
 
  jest.spyOn(window, "addEventListener");
});
 
 
// ...

❶ 回复错误请求到 GET /inventory

注意这里公开了应用程序加载初始数据后将解决的承诺,因为您需要知道要等待什么。

或者,您可以等待测试中的固定超时,或继续重试直到成功或超时。这些替代方案不会要求您导出 loadInitialData 返回的承诺,但它们会使您的测试变得不稳定或比应有的速度更慢。

您不必担心 main.js 中 module.exports 的分配,因为在使用 Browserify 构建后在浏览器中运行该文件时,它不会产生任何影响。 Browserify 将为您处理所有 module.exports 分配,将所有依赖项打包到一个 bundle.js 中。

既然您已经学会了如何使用 nock 拦截器来测试涉及 HTTP 请求的功能,并在必要时覆盖 fetch,我将以挑战结束本节。

目前,在撤消操作时,您的应用程序不会向服务器发送更新清单内容的请求。作为练习,尝试使撤消功能与服务器同步,并测试此集成。为了您能够实现此功能,我在 GitHub 上本章的服务器文件夹中添加了一个新的 DELETE /inventory/:itemName 路由到服务器,该路由包含一个包含用户想要删除的数量的正文。

在本节结束时,您应该能够通过使用 nock 准确模拟客户端的行为,将客户端的测试与后端隔离。多亏了 nock,您可以专注于指定您的服务器在哪种情况下会产生的响应,而无需启动整个后端。创建这样的隔离测试可以让团队中的每个人更快、更轻松地运行测试。这种改进加速了开发人员收到的反馈循环,因此,激励他们编写更好的测试并更频繁地进行测试,这反过来往往会导致更可靠的软件。

6.5.2 涉及 WebSocket 的测试
到目前为止,如果您的应用程序一次只有一个用户,它可以无缝运行。但是,如果多个操作员需要同时管理库存怎么办?如果是这种情况,库存很容易不同步,导致每个操作员看到不同的项目和数量。

为了解决这个问题,您将通过 WebSockets 实现对实时更新的支持。这些 WebSocket 将负责在库存数据更改时更新每个客户端,以便在客户端之间始终保持同步。

因为这本书是关于测试的,我已经在后端实现了这个功能。如果你不想自己实现它,你可以使用你可以在本书的 GitHub 存储库中的第 6 章文件夹中找到的服务器,网址为 https://github.com/lucasfcosta/testing-javascript-applications。

当客户端添加项目时,我对服务器所做的更改将导致它向所有连接的客户端发出 add_item 事件,除了发送请求的客户端。

要连接到服务器,您将使用 socket.io-client 模块,因此您必须使用 npm install socket.io-client 将其安装为依赖项。

通过创建将连接到服务器并在连接后保存客户端 ID 的模块,开始实现实时更新功能。

const { API_ADDR } = require("./inventoryController");
 
const client = { id: null };
 
const io = require("socket.io-client");
 
const connect = () => {
  return new Promise(resolve => {
    const socket = io(API_ADDR);         ❶
 
    socket.on("connect", () => {         ❷
      client.id = socket.id;
      resolve(socket);
    });
  });
}
 
module.exports = { client, connect };

❶ 创建一个连接到 API_ADDR 的客户端实例

❷ 客户端连接后,存储其 id 并解析 promise

每个客户端要连接到服务器,必须在 main.js 中调用 socket.js 导出的 connect 函数。

const { connect } = require("./socket");
 
// ...
 
connect();                             ❶
 
module.exports = loadInitialData();

❶ 应用程序加载时连接到 Socket.io 服务器

客户端连接到服务器后,每当用户添加新项目时,客户端必须通过 x-socket-client-id 头将其 Socket.io 客户端 ID 发送到服务器。 服务器将使用此标头来识别哪个客户端添加了该项目,以便它可以跳过它,因为该客户端已经更新了自己。

注意 允许客户端向库存添加项目的路由将提取 x-socket-client-id 标头中的值以确定哪个客户端发送了请求。 然后,一旦将项目添加到清单中,它将遍历所有连接的套接字并向 id 与 x-socket-client-id 中的不匹配的客户端发出 add_item 事件。

router.post("/inventory/:itemName", async ctx => {
  const clientId = ctx.request.headers["x-socket-client-id"];
 
  // ...
 
  Object.entries(io.socket.sockets.connected)
    .forEach(([id, socket]) => {
      if (id === clientId) return;
      socket.emit("add_item", { itemName, quantity });
    });
 
  // ...
});

更新inventoryController.js,以便它将客户端的ID发送到服务器,如下所示。

// ...
 
const addItem = (itemName, quantity) => {
  const { client } = require("./socket");
 
  // ...
 
  fetch(`${API_ADDR}/inventory/${itemName}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-socket-client-id": client.id             ❶
    },
    body: JSON.stringify({ quantity })
  });
 
  return data.inventory;
};

❶ 包含一个 x-socket-client-id,其中包含 Socket.io 客户端在发送添加项目的请求时的 ID

现在服务器可以识别发送者,最后一步是更新 socket.js 文件,以便客户端可以在收到其他人添加项目时服务器发送的 add_item 消息时更新自己。 这些消息包含一个 itemName 和一个数量属性,您将使用它们来更新库存数据。 一旦本地状态是最新的,您将使用它来更新 DOM。


const { API_ADDR, data } = require("./inventoryController");
const { updateItemList } = require("./domController");
 
// ...

const handleAddItemMsg = ({ itemName, quantity }) => {             ❶
  const currentQuantity = data.inventory[itemName] || 0;
  data.inventory[itemName] = currentQuantity + quantity;
  return updateItemList(data.inventory);
};
 
const connect = () => {
  return new Promise(resolve => {
    // ...
 
    socket.on("add_item", handleAddItemMsg);                       ❷
  });
};
 
module.exports = { client, connect };

❶ 一个函数,用于更新应用程序的状态和给定包含项目名称和数量的对象的项目列表

❷ 当服务器发出 add_item 事件时调用 handleAddItemMsg

通过 npm run build 使用 Browserify 重建您的 bundle.js 并使用 npx http-server ./ 为其提供服务,来尝试这些更改。 不要忘记您的服务器必须在 API_ADDR 中指定的地址上运行。

可以在多个集成级别上测试此功能。 例如,您可以单独检查您的 handleAddItemMsg 函数,而根本不涉及 WebSockets。

要单独测试 handleAddItemMsg,首先在 socket.js 中导出它。

// ...
 
module.exports = { client, connect, handleAddItemMsg };

然后,在新的socket.test.js中导入,直接调用,传入一个包含itemName和数量的对象。 不要忘记您需要挂钩来确保在每次测试之前重置文档和库存状态。

const fs = require("fs");
const initialHtml = fs.readFileSync("./index.html");
const { getByText } = require("@testing-library/dom");
const { data } = require("./inventoryController");
 
const { handleAddItemMsg } = require("./socket");
 
beforeEach(() => {
  document.body.innerHTML = initialHtml;
});
 
beforeEach(() => {
  data.inventory = {};
});

describe("handleAddItemMsg", () => {
  test("updating the inventory and the item list", () => {
    handleAddItemMsg({ itemName: "cheesecake", quantity: 6 });       ❶
 
    expect(data.inventory).toEqual({ cheesecake: 6 });
    const itemList = document.getElementById("item-list");
    expect(itemList.childNodes).toHaveLength(1);
    expect(getByText(itemList, "cheesecake - Quantity: 6"))
      .toBeInTheDocument();
  });
});

❶ 调用handleAddItemMsg函数直接测试

提示 尽管此测试对您在迭代时获得反馈很有用,但它与通过 WebSockets 发送 add_item 消息而不是直接调用 handleAddItemMsg 的测试高度重叠。因此,在实际场景中,在选择是否保留之前,请考虑您的时间和成本限制。

正如我之前提到的,准确复制运行时场景将使您的测试产生更可靠的保证。在这种情况下,您可以模拟后端发送的更新最接近的是创建一个 Socket.io 服务器并自己调度更新。然后,您可以检查这些更新是否在您的客户端中触发了所需的效果。

因为在运行测试时需要 Socket.io 服务器,所以使用 npm install --save-dev socket.io 将其安装为开发依赖项。

安装 Socket.io 后,创建一个名为 testSocketServer.js 的文件,您将在其中创建自己的 Socket.io 服务器。此文件应导出用于启动和停止服务器的函数以及向客户端发送消息的函数。

const server = require("http").createServer();
const io = require("socket.io")(server);             ❶
 
const sendMsg = (msgType, content) => {              ❷
  io.sockets.emit(msgType, content);
};
 
const start = () =>                                  ❸
  new Promise(resolve => {
    server.listen(3000, resolve);
  });
 
const stop = () =>                                   ❹
  new Promise(resolve => {
    server.close(resolve);
  });
 
module.exports = { start, stop, sendMsg };

❶ 创建一个socket.io服务器

❷ 向连接到socket.io服务器的客户端发送消息的函数

❸ 在端口 3000 上启动 socket.io 服务器,并在它启动后解析一个 promise

❹ 关闭 socket.io 服务器,并在停止后解析承诺

注意 理想情况下,您应该有一个单独的常量来确定您的服务器应该侦听的端口。如果需要,您可以将 API_ADDR 分成 API_HOST 和 API_PORT。因为本书侧重于测试,所以我在这里对 3000 进行了硬编码。

此外,为了避免由于服务器已绑定到端口 3000 而无法运行测试,允许用户通过环境变量配置此端口可能会很有用。

返回在 start 和 stop 结束时解析的 promise 是至关重要的,这样您就可以在钩子中使用它们时等待它们完成。否则,您的测试可能会因资源挂起而挂起。

最后,是时候编写一个测试,通过 Socket.io 服务器发送消息并检查您的应用程序是否正确处理它们。

从将启动服务器的钩子开始,将您的客户端连接到它,然后在测试完成后关闭服务器。

const nock = require("nock");
 
// ...
 
const { start, stop } = require("./testSocketServer");
 
// ...
 
describe("handling real messages", () => {
  beforeAll(start);                             ❶
 
  beforeAll(async () => {
    nock.cleanAll();                            ❷
    await connect();                            ❸
  });
 
  afterAll(stop);                               ❹
});

❶ 在测试运行之前,启动你的 Socket.io 测试服务器

❷ 为避免 nock 干扰您与 Socket.io 服务器的连接,请在尝试连接之前清除所有模拟

❸ 在所有测试之前,连接到 Socket.io 测试服务器

❹ 测试完成后,停止 Socket.io 测试服务器

最后,编写一个发送 add_item 消息的测试,等待一秒钟以便客户端可以接收和处理它,并检查新的应用程序状态是否与您期望的状态相匹配。

const { start, stop, sendMsg } = require("./testSocketServer");
 
// ...

describe("handling real messages", () => {

  // ...
 
  test("handling add_item messages", async () => {
    sendMsg("add_item", { itemName: "cheesecake", quantity: 6 });          ❶
 
    await new Promise(resolve => setTimeout(resolve, 1000));               ❷
 
    expect(data.inventory).toEqual({ cheesecake: 6 });                     ❸
    const itemList = document.getElementById("item-list");                 ❸
    expect(itemList.childNodes).toHaveLength(1);                           ❸
    expect(getByText(itemList, "cheesecake - Quantity: 6"))                ❸
      .toBeInTheDocument();                                                ❸
  });                     
});

❶ 通过 Socket.io 测试服务器发送消息

❷ 等待消息被处理

❸ 检查页面状态是否符合预期状态

请注意此测试与 handleAddItemMsg 的单元测试有多少重叠。两者兼有的好处是,如果连接设置出现问题,使用真实套接字的测试将失败,但单元测试不会。因此,您可以快速检测问题是出在逻辑上还是出在服务器连接上。两者兼而有之的问题在于它们会增加维护测试套件的额外成本,特别是考虑到您在两个测试中执行相同的断言。

现在您已经检查了您的应用程序在接收消息时是否可以更新,编写一个测试来检查库存控制器.js 中的 handleAddItem 函数是否将套接字客户端的 ID 包含在它发送到服务器的 POST 请求中。该测试不同部分之间的通信如图 6.9 所示。


图6-9

为此,您必须启动您的测试服务器,连接到它,并针对 nock 拦截器执行 handleAddItem 函数,它将仅匹配包含足够 x-socket-client-id 标头的请求。

// ...
 
const { start, stop } = require("./testSocketServer");
const { client, connect } = require("./socket");
 
// ...
 
describe("live-updates", () => {
  beforeAll(start);
 
  beforeAll(async () => {
    nock.cleanAll();
    await connect();
  });
 
  afterAll(stop);
 
  test("sending a x-socket-client-id header", () => {
    const clientId = client.id;
 
    nock(API_ADDR, {                                              ❶
        reqheaders: { "x-socket-client-id": clientId }
    })
      .post(/inventory\/.*$/)
      .reply(200);
 
    addItem("cheesecake", 5);
  });
});

❶ 仅成功响应包含 x-socket-client-id 标头的 POST /inventory/:itemName 请求

重要的是要看到,在这些示例中,我们并没有试图在测试中复制后端的行为。我们分别检查我们发送的请求和我们是否可以处理收到的消息。检查后端是否将正确的消息发送到正确的客户端是应该在服务器的测试中进行的验证,而不是客户端的测试。

既然您已经了解了如何设置可在测试中使用的 Socket.io 服务器以及如何验证 WebSockets 集成,请尝试使用新功能扩展此应用程序并对其进行测试。请记住,您可以通过单独检查处理程序函数或通过测试服务器推送真实消息来在多个不同的集成级别编写这些测试。例如,尝试在客户端单击撤消按钮时推送实时更新,或者尝试添加一个测试来检查页面加载时 main.js 是否连接到服务器。

以 WebSocket 为例,您一定已经学会了如何模拟前端可能与其他应用程序进行的其他类型的交互。如果您有存根会导致过多维护开销的依赖项,则最好实现您自己的依赖项实例——您可以完全控制的实例。例如,在这种情况下,手动操作多个不同的间谍来访问侦听器和触发事件会导致过多的维护开销。除了使您的测试更难阅读和维护之外,它还会使它们与运行时发生的情况更加不同,从而导致您的可靠性保证更弱。这种方法的缺点是您的测试范围会增加,从而使您获得反馈的时间更长,并且变得更加粗糙。因此,在决定适合您情况的最佳技术时,您必须谨慎。

概括

JavaScript 在浏览器中可以访问的值和 API 与它在 Node.js 中可以访问的值和 API 不同。因为 Jest 只能在 Node.js 中运行,所以在使用 Jest 运行测试时,您必须准确地复制浏览器的环境。

为了模拟浏览器的环境,Jest 使用了 JSDOM,这是一种完全用 JavaScript 编写的 Web 标准的实现。 JSDOM 使您可以在其他运行时环境(如 Node.js)中访问浏览器 API。

在多个集成级别编写测试需要您将代码组织成单独的部分。为了在测试中轻松管理不同的模块,您仍然可以使用 require,但是您必须使用像 Browserify 或 Webpack 这样的打包器将您的依赖项打包到可以在浏览器中运行的文件中。

在您的测试中,感谢 JSDOM,您可以访问诸如 document.querySelector 和 document.getElementById 之类的 API。一旦你使用了你想要测试的函数,就可以使用这些 API 在页面中的 DOM 节点上查找和断言。
通过 ID 或它们在 DOM 中的位置查找元素可能会导致您的测试变得脆弱并且与您的标记耦合得太紧。为避免这些问题,请使用诸如 dom-testing-library 之类的工具通过元素的内容或其他属性来查找元素,这些属性是元素应有的组成部分,例如其角色或标签。

为了编写更准确和可读的断言,而不是手动访问 DOM 元素的属性或编写复杂的代码来执行某些检查,而是使用 jest-dom 之类的库来扩展 Jest,并使用专门针对 DOM 的新断言。

浏览器对复杂的用户交互做出反应,例如打字、点击和滚动。为了处理这些交互,浏览器依赖于事件。由于测试在准确模拟运行时发生的情况时更可靠,因此您的测试应尽可能精确地模拟事件。

准确重现事件的一种方法是使用来自 dom-testing-library 的 fireEvent 函数或由 test-library 组织下的另一个库 user-event 提供的实用程序。

您可以在不同的集成级别测试事件及其处理程序。如果您在编写代码时需要更精细的反馈,可以通过直接调用处理程序来测试它们。如果您想用细粒度的反馈换取更可靠的保证,您可以改为发送真实事件。

如果您的应用程序使用诸如 History 或 Web Storage API 之类的 Web API,您可以在测试中使用它们的 JSDOM 实现。请记住,您不应自己测试这些 API;您应该测试您的应用程序是否与它们充分交互。

为了避免使您的测试设置过程更加复杂,并摆脱启动后端以运行前端测试的必要性,请使用 nock 拦截请求。使用 nock,您可以确定拦截哪些路由以及这些拦截器将产生哪些响应。

与我们见过的所有其他类型的测试类似,WebSockets 可以在不同的集成级别进行测试。您可以编写直接调用处理程序函数的测试,或者您可以创建一个服务器,通过它您将发送真实的消息。

你可能感兴趣的:(6.5 处理 WebSockets 和 HTTP 请求)